mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-11 02:33:51 +00:00
Merge pull request #106 from freqtrade/echarts_pairhistory
Echarts pairhistory
This commit is contained in:
commit
c53e0f89d2
|
@ -26,6 +26,7 @@
|
|||
"vuex-class": "^0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/echarts": "^4.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^4.2.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.6",
|
||||
|
@ -49,6 +50,7 @@
|
|||
"sass-loader": "^10.0.2",
|
||||
"typescript": "~4.0.2",
|
||||
"vue-cli-plugin-bootstrap-vue": "~0.6.0",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuex-class": "^0.3.2"
|
||||
}
|
||||
}
|
||||
|
|
527
src/components/charts/CandleChart.vue
Normal file
527
src/components/charts/CandleChart.vue
Normal file
|
@ -0,0 +1,527 @@
|
|||
<template>
|
||||
<div class="row flex-grow-1 chart-wrapper">
|
||||
<v-chart v-if="hasData" theme="dark" autoresize :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
|
||||
import ECharts from 'vue-echarts';
|
||||
import * as echarts from 'echarts';
|
||||
import { Trade, PairHistory, PlotConfig } from '@/types';
|
||||
import randomColor from '@/shared/randomColor';
|
||||
import { roundTimeframe } from '@/shared/timemath';
|
||||
|
||||
import 'echarts';
|
||||
|
||||
// Chart default options
|
||||
const MARGINLEFT = '4%';
|
||||
const MARGINRIGHT = '1%';
|
||||
// const upColor = '#00da3c';
|
||||
// const downColor = '#ec0000';
|
||||
// const upBorderColor = '#008F28';
|
||||
// const downBorderColor = '#8A0000';
|
||||
|
||||
// Binance colors
|
||||
const upColor = '#2ed191';
|
||||
const upBorderColor = '#19d189';
|
||||
const downColor = '#f84960';
|
||||
const downBorderColor = '#e33249';
|
||||
|
||||
@Component({
|
||||
components: { 'v-chart': ECharts },
|
||||
})
|
||||
export default class CandleChart extends Vue {
|
||||
@Prop({ required: false, default: [] }) readonly trades!: Array<Trade>;
|
||||
|
||||
@Prop({ required: true }) readonly dataset!: PairHistory;
|
||||
|
||||
@Prop({ default: true }) readonly useUTC!: boolean;
|
||||
|
||||
@Prop({ required: true }) plotConfig!: PlotConfig;
|
||||
|
||||
// Only recalculate buy / sell data if necessary
|
||||
signalsCalculated = false;
|
||||
|
||||
buyData = [] as Array<number>[];
|
||||
|
||||
sellData = [] as Array<number>[];
|
||||
|
||||
@Watch('timeframe')
|
||||
timeframeChanged() {
|
||||
this.signalsCalculated = false;
|
||||
}
|
||||
|
||||
@Watch('dataset')
|
||||
datasetChanged() {
|
||||
this.signalsCalculated = false;
|
||||
}
|
||||
|
||||
get strategy() {
|
||||
return this.dataset ? this.dataset.strategy : '';
|
||||
}
|
||||
|
||||
get pair() {
|
||||
return this.dataset ? this.dataset.pair : '';
|
||||
}
|
||||
|
||||
get timeframe() {
|
||||
return this.dataset ? this.dataset.timeframe : '';
|
||||
}
|
||||
|
||||
get timeframems() {
|
||||
return this.dataset ? this.dataset.timeframe_ms : 0;
|
||||
}
|
||||
|
||||
get datasetColumns() {
|
||||
return this.dataset ? this.dataset.columns : [];
|
||||
}
|
||||
|
||||
get hasData() {
|
||||
return this.dataset !== null && typeof this.dataset === 'object';
|
||||
}
|
||||
|
||||
get filteredTrades() {
|
||||
return this.trades.filter((item: Trade) => item.pair === this.pair);
|
||||
}
|
||||
|
||||
get chartOptions() {
|
||||
if (!this.hasData) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// console.log(`Available Columns: ${this.dataset.columns}`);
|
||||
// Find default columns (sequence might be different, depending on the strategy)
|
||||
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;
|
||||
|
||||
console.log(`subplotcount: ${subplotCount}`);
|
||||
|
||||
// Always show ~250 candles max as starting point
|
||||
const startingZoom = (1 - 250 / this.dataset.length) * 100;
|
||||
|
||||
const options: echarts.EChartOption = {
|
||||
title: {
|
||||
text: `${this.strategy} - ${this.pair} - ${this.timeframe}`,
|
||||
show: true,
|
||||
},
|
||||
backgroundColor: '#1b1b1b',
|
||||
useUTC: this.useUTC,
|
||||
dataset: {
|
||||
source: this.dataset.data,
|
||||
},
|
||||
animation: false,
|
||||
legend: {
|
||||
data: ['Candles', 'Volume', 'Buy', 'Sell'],
|
||||
right: '1%',
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
axisPointer: {
|
||||
link: [{ xAxisIndex: 'all' }],
|
||||
label: {
|
||||
backgroundColor: '#777',
|
||||
},
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'time',
|
||||
scale: true,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: true },
|
||||
axisLabel: { show: true },
|
||||
position: 'top',
|
||||
splitLine: { show: false },
|
||||
splitNumber: 20,
|
||||
min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
},
|
||||
{
|
||||
type: 'time',
|
||||
gridIndex: 1,
|
||||
scale: true,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false },
|
||||
splitLine: { show: false },
|
||||
splitNumber: 20,
|
||||
min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 1,
|
||||
splitNumber: 2,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
grid: [
|
||||
{
|
||||
left: MARGINLEFT,
|
||||
right: MARGINRIGHT,
|
||||
// Grid Layout from bottom to top
|
||||
bottom: `${subplotCount * 10 + 10}%`,
|
||||
},
|
||||
{
|
||||
// Volume
|
||||
left: MARGINLEFT,
|
||||
right: MARGINRIGHT,
|
||||
// Grid Layout from bottom to top
|
||||
bottom: `${subplotCount * 10 + 5}%`,
|
||||
height: '10%',
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0, 1],
|
||||
start: startingZoom,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
xAxisIndex: [0, 1],
|
||||
type: 'slider',
|
||||
bottom: 10,
|
||||
start: startingZoom,
|
||||
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,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Buy',
|
||||
type: 'scatter',
|
||||
symbol: 'triangle',
|
||||
symbolSize: 10,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: '#00ff26',
|
||||
},
|
||||
encode: {
|
||||
x: colDate,
|
||||
y: colBuyData,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Sell',
|
||||
type: 'scatter',
|
||||
symbol: 'diamond',
|
||||
symbolSize: 8,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: '#FF0000',
|
||||
},
|
||||
encode: {
|
||||
x: colDate,
|
||||
y: colSellData,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// this.createSignalData(colDate, colOpen, colBuy, colSell);
|
||||
|
||||
// This will be merged into final plot config
|
||||
// const subPlots = {
|
||||
// legend: [] as string[],
|
||||
// grid: [] as object[],
|
||||
// yaxis: [] as object[],
|
||||
// xaxis: [] as object[],
|
||||
// xaxisIndex: [] as number[],
|
||||
// series: [] as object[],
|
||||
// };
|
||||
|
||||
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 (options.legend && options.legend.data) {
|
||||
options.legend.data.push(key);
|
||||
}
|
||||
const sp: echarts.EChartOption.Series = {
|
||||
name: key,
|
||||
type: value.type || 'line',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: value.color,
|
||||
},
|
||||
encode: {
|
||||
x: colDate,
|
||||
y: col,
|
||||
},
|
||||
showSymbol: false,
|
||||
};
|
||||
if (options.series) {
|
||||
options.series.push(sp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// START Subplots
|
||||
if ('subplots' in this.plotConfig) {
|
||||
let plotIndex = 2;
|
||||
Object.entries(this.plotConfig.subplots).forEach(([key, value]) => {
|
||||
// define yaxis
|
||||
if (options.yAxis && Array.isArray(options.yAxis)) {
|
||||
options.yAxis.push({
|
||||
scale: true,
|
||||
gridIndex: plotIndex,
|
||||
name: key,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 60,
|
||||
axisLabel: { show: true },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
});
|
||||
}
|
||||
if (options.xAxis && Array.isArray(options.xAxis)) {
|
||||
options.xAxis.push({
|
||||
type: 'time',
|
||||
scale: true,
|
||||
gridIndex: plotIndex,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false },
|
||||
splitLine: { show: false },
|
||||
splitNumber: 20,
|
||||
});
|
||||
}
|
||||
if (options.dataZoom) {
|
||||
options.dataZoom.forEach((el) =>
|
||||
el.xAxisIndex && Array.isArray(el.xAxisIndex) ? el.xAxisIndex.push(plotIndex) : null,
|
||||
);
|
||||
}
|
||||
if (options.grid && Array.isArray(options.grid)) {
|
||||
options.grid.push({
|
||||
left: MARGINLEFT,
|
||||
right: MARGINRIGHT,
|
||||
bottom: `${plotIndex * 8}%`,
|
||||
height: '8%',
|
||||
});
|
||||
}
|
||||
Object.entries(value).forEach(([sk, sv]) => {
|
||||
if (options.legend && options.legend.data && Array.isArray(options.legend.data)) {
|
||||
options.legend.data.push(sk);
|
||||
}
|
||||
// entries per subplot
|
||||
const col = this.dataset.columns.findIndex((el) => el === sk);
|
||||
if (col > 0) {
|
||||
const sp: echarts.EChartOption.Series = {
|
||||
name: sk,
|
||||
type: sv.type || 'line',
|
||||
xAxisIndex: plotIndex,
|
||||
yAxisIndex: plotIndex,
|
||||
itemStyle: {
|
||||
color: sv.color || randomColor(),
|
||||
},
|
||||
encode: {
|
||||
x: colDate,
|
||||
y: col,
|
||||
},
|
||||
showSymbol: false,
|
||||
};
|
||||
if (options.series && Array.isArray(options.series)) {
|
||||
options.series.push(sp);
|
||||
}
|
||||
} else {
|
||||
console.log(`element ${sk} was not found in the columns.`);
|
||||
}
|
||||
});
|
||||
|
||||
plotIndex += 1;
|
||||
});
|
||||
}
|
||||
// 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 (options.grid && Array.isArray(options.grid)) {
|
||||
// Last subplot is bottom
|
||||
options.grid[options.grid.length - 1].bottom = '50px';
|
||||
delete options.grid[options.grid.length - 1].top;
|
||||
}
|
||||
|
||||
if (this.filteredTrades.length > 0) {
|
||||
// Show trades
|
||||
const trades: Array<string | number>[] = [];
|
||||
const tradesClose: Array<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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log(`Trades: ${trades.length}`);
|
||||
// console.log(trades);
|
||||
// console.log(`ClosesTrades: ${tradesClose.length}`);
|
||||
// console.log(tradesClose);
|
||||
|
||||
const name = 'Trades';
|
||||
const nameClose = 'Trades Close';
|
||||
if (options.legend && options.legend.data) {
|
||||
options.legend.data.push(name);
|
||||
}
|
||||
const sp: echarts.EChartOption.SeriesScatter = {
|
||||
name,
|
||||
type: 'scatter',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: 'cyan',
|
||||
},
|
||||
data: trades,
|
||||
};
|
||||
if (options.series) {
|
||||
options.series.push(sp);
|
||||
}
|
||||
if (options.legend && options.legend.data) {
|
||||
options.legend.data.push(nameClose);
|
||||
}
|
||||
const closeSeries: echarts.EChartOption.SeriesScatter = {
|
||||
name: nameClose,
|
||||
type: 'scatter',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: 'pink',
|
||||
},
|
||||
data: tradesClose,
|
||||
};
|
||||
if (options.series) {
|
||||
options.series.push(closeSeries);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(options);
|
||||
// TODO: Rebuilding this causes a full redraw for every new step
|
||||
return options;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.echarts {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
/* TODO: height calculation is not working correctly - uses min-height for now */
|
||||
/* height: 600px; */
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
165
src/components/charts/CandleChartContainer.vue
Normal file
165
src/components/charts/CandleChartContainer.vue
Normal file
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<div class="container-fluid flex-column align-items-stretch d-flex h-100">
|
||||
<b-modal
|
||||
id="plotConfiguratorModal"
|
||||
title="Plot Configurator"
|
||||
ok-only
|
||||
hide-backdrop
|
||||
button-size="sm"
|
||||
>
|
||||
<PlotConfigurator v-model="plotConfig" :columns="datasetColumns" />
|
||||
</b-modal>
|
||||
|
||||
<div class="row mr-0">
|
||||
<div class="col-mb-2 ml-2">
|
||||
<b-select v-model="pair" :options="availablePairs" size="sm" @change="refresh"> </b-select>
|
||||
</div>
|
||||
<div class="col-mb-2 ml-2 mr-2">
|
||||
<b-button :disabled="!!!pair" size="sm" @click="refresh">↻</b-button>
|
||||
</div>
|
||||
<div v-if="hasDataset" class="col-mb-2 ml-2 mr-2">
|
||||
<small>Buysignals: {{ dataset.buy_signals }}</small>
|
||||
<small class="ml-2">SellSignals: {{ dataset.sell_signals }}</small>
|
||||
</div>
|
||||
<div class="col-mb-2 ml-auto mr-2">
|
||||
<b-select
|
||||
v-model="plotConfigName"
|
||||
:options="availablePlotConfigNames"
|
||||
size="sm"
|
||||
@change="plotConfigChanged"
|
||||
>
|
||||
</b-select>
|
||||
</div>
|
||||
|
||||
<div class="col-mb-2 mr-2">
|
||||
<b-checkbox v-model="useUTC" title="Use UTC for graph">useUTC</b-checkbox>
|
||||
</div>
|
||||
<div class="col-mb-2 mr-1">
|
||||
<b-button size="sm" title="Plot configurator" @click="showConfigurator">⚙</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mr-1 ml-1 h-100">
|
||||
<CandleChart
|
||||
v-if="hasDataset"
|
||||
:dataset="dataset"
|
||||
:trades="trades"
|
||||
:plot-config="plotConfig"
|
||||
:use-u-t-c="useUTC"
|
||||
>
|
||||
</CandleChart>
|
||||
<label v-else style="margin: auto auto; font-size: 1.5rem">No data available</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import {
|
||||
Trade,
|
||||
PairHistory,
|
||||
EMPTY_PLOTCONFIG,
|
||||
PlotConfig,
|
||||
PairCandlePayload,
|
||||
PairHistoryPayload,
|
||||
} from '@/types';
|
||||
import CandleChart from '@/components/charts/CandleChart.vue';
|
||||
import PlotConfigurator from '@/components/charts/PlotConfigurator.vue';
|
||||
import { getCustomPlotConfig, getPlotConfigName } from '@/shared/storage';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({ components: { CandleChart, PlotConfigurator } })
|
||||
export default class CandleChartContainer extends Vue {
|
||||
@Prop({ required: true }) readonly availablePairs!: string[];
|
||||
|
||||
@Prop({ required: true }) readonly timeframe!: string;
|
||||
|
||||
@Prop({ required: false, default: [] }) readonly trades!: Array<Trade>;
|
||||
|
||||
@Prop({ required: false, default: false }) historicView!: boolean;
|
||||
|
||||
/** Only required if historicView is true */
|
||||
@Prop({ required: false, default: false }) timerange!: string;
|
||||
|
||||
/**
|
||||
* Only required if historicView is true
|
||||
*/
|
||||
@Prop({ required: false, default: false }) strategy!: string;
|
||||
|
||||
pair = '';
|
||||
|
||||
useUTC = true;
|
||||
|
||||
plotConfig: PlotConfig = { ...EMPTY_PLOTCONFIG };
|
||||
|
||||
plotConfigName = '';
|
||||
|
||||
@ftbot.State availablePlotConfigNames!: Array<string>;
|
||||
|
||||
@ftbot.Action setPlotConfigName;
|
||||
|
||||
@ftbot.State candleData!: PairHistory;
|
||||
|
||||
@ftbot.State history!: PairHistory;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action public getPairCandles!: (payload: PairCandlePayload) => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action public getPairHistory!: (payload: PairHistoryPayload) => void;
|
||||
|
||||
get dataset(): PairHistory {
|
||||
if (this.historicView) {
|
||||
return this.history[`${this.pair}__${this.timeframe}`];
|
||||
}
|
||||
return this.candleData[`${this.pair}__${this.timeframe}`];
|
||||
}
|
||||
|
||||
get datasetColumns() {
|
||||
return this.dataset ? this.dataset.columns : [];
|
||||
}
|
||||
|
||||
get hasDataset(): boolean {
|
||||
return !!this.dataset;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.plotConfigName = getPlotConfigName();
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
|
||||
}
|
||||
|
||||
plotConfigChanged() {
|
||||
console.log('plotConfigChanged');
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
|
||||
this.setPlotConfigName(this.plotConfigName);
|
||||
}
|
||||
|
||||
showConfigurator() {
|
||||
this.$bvModal.show('plotConfiguratorModal');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this.pair && this.timeframe) {
|
||||
if (this.historicView) {
|
||||
this.getPairHistory({
|
||||
pair: this.pair,
|
||||
timeframe: this.timeframe,
|
||||
timerange: this.timerange,
|
||||
strategy: this.strategy,
|
||||
});
|
||||
} else {
|
||||
this.getPairCandles({ pair: this.pair, timeframe: this.timeframe, limit: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('availablePairs')
|
||||
watchAvailablePairs() {
|
||||
[this.pair] = this.availablePairs;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
348
src/components/charts/PlotConfigurator.vue
Normal file
348
src/components/charts/PlotConfigurator.vue
Normal file
|
@ -0,0 +1,348 @@
|
|||
<template>
|
||||
<div v-if="columns">
|
||||
<div class="col-mb-3 ml-2">
|
||||
<b-form-radio-group
|
||||
v-model="plotOption"
|
||||
class="w-100"
|
||||
:options="plotOptions"
|
||||
buttons
|
||||
button-variant="outline-primary"
|
||||
>
|
||||
</b-form-radio-group>
|
||||
</div>
|
||||
<div v-if="plotOption == 'subplots'" class="col-mb-3">
|
||||
<hr />
|
||||
|
||||
<b-form-group label="Subplot" label-for="FieldSel">
|
||||
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="4">
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<b-form-group v-if="plotOption == 'subplots'" label="New subplot" label-for="newSubplot">
|
||||
<b-input-group size="sm">
|
||||
<b-form-input id="newSubplot" v-model="newSubplotName" class="addPlot"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button @click="addSubplot">+</b-button>
|
||||
<b-button v-if="selSubPlot" @click="delSubplot">-</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<b-form-group class="col" label="Add indicator" label-for="indicatorSelector">
|
||||
<b-form-select
|
||||
id="indicatorSelector"
|
||||
v-model="selAvailableIndicator"
|
||||
:options="columns"
|
||||
:select-size="4"
|
||||
>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
<div class="col-1 px-0 text-center">
|
||||
<b-button
|
||||
class="mt-5"
|
||||
variant="primary"
|
||||
title="Add indicator to plot"
|
||||
size="sm"
|
||||
:disabled="!selAvailableIndicator"
|
||||
@click="addIndicator"
|
||||
>
|
||||
>
|
||||
</b-button>
|
||||
<b-button
|
||||
variant="primary"
|
||||
title="Remove indicator to plot"
|
||||
size="sm"
|
||||
:disabled="!selIndicator"
|
||||
@click="removeIndicator"
|
||||
>
|
||||
<
|
||||
</b-button>
|
||||
</div>
|
||||
<b-form-group class="col" label="Used indicators" label-for="selectedIndicators">
|
||||
<b-form-select
|
||||
id="selectedIndicators"
|
||||
v-model="selIndicator"
|
||||
:options="usedColumns"
|
||||
:select-size="4"
|
||||
>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<b-form-group label="Choose type" label-for="plotTypeSelector">
|
||||
<b-form-select id="plotTypeSelector" v-model="graphType" :options="availableGraphTypes">
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
<hr />
|
||||
|
||||
<b-form-group label="Color" label-for="colsel" size="sm">
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<div :style="{ 'background-color': selColor }" class="colorbox mr-2"></div>
|
||||
</b-input-group-prepend>
|
||||
<b-form-input id="colsel" v-model="selColor" size="sm"> </b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="primary" size="sm" @click="newColor">↻</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<hr />
|
||||
<b-form-group label="Plot config name" label-for="idPlotConfigName">
|
||||
<b-form-input id="idPlotConfigName" v-model="plotConfigName" :options="availableGraphTypes">
|
||||
</b-form-input>
|
||||
</b-form-group>
|
||||
<div class="row">
|
||||
<b-button class="ml-3" variant="primary" size="sm" @click="loadPlotConfig">Load</b-button>
|
||||
<b-button class="ml-1" variant="primary" size="sm" @click="loadPlotConfigFromStrategy">
|
||||
Load from strategy
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
class="ml-1"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
data-toggle="tooltip"
|
||||
title="Save configuration"
|
||||
@click="savePlotConfig"
|
||||
>Save</b-button
|
||||
>
|
||||
<b-button
|
||||
id="showButton"
|
||||
class="ml-1"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
title="Show configuration for easy transfer to a strategy"
|
||||
@click="showConfig = !showConfig"
|
||||
>Show</b-button
|
||||
>
|
||||
<b-button
|
||||
v-if="showConfig"
|
||||
class="ml-1"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
title="Load configuration from text box below"
|
||||
@click="loadConfigFromString"
|
||||
>Load from String</b-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="showConfig" class="col-mb-5 ml-2 mt-2">
|
||||
<b-textarea
|
||||
id="TextArea"
|
||||
v-model="plotConfigJson"
|
||||
class="textArea"
|
||||
size="sm"
|
||||
:state="tempPlotConfigValid"
|
||||
>
|
||||
</b-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { PlotConfig, EMPTY_PLOTCONFIG } from '@/types';
|
||||
import randomColor from '@/shared/randomColor';
|
||||
import { getCustomPlotConfig } from '@/shared/storage';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({})
|
||||
export default class PlotConfigurator extends Vue {
|
||||
@Prop({ required: true }) value!: PlotConfig;
|
||||
|
||||
@Prop({ required: true }) columns!: Array<string>;
|
||||
|
||||
@Emit('input')
|
||||
emitPlotConfig() {
|
||||
return this.plotConfig;
|
||||
}
|
||||
|
||||
@ftbot.Action getStrategyPlotConfig;
|
||||
|
||||
@ftbot.State strategyPlotConfig;
|
||||
|
||||
plotConfig: PlotConfig = EMPTY_PLOTCONFIG;
|
||||
|
||||
plotOptions = [
|
||||
{ text: 'Main Plot', value: 'main_plot' },
|
||||
{ text: 'Subplots', value: 'subplots' },
|
||||
];
|
||||
|
||||
plotOption = 'main_plot';
|
||||
|
||||
plotConfigName = 'default';
|
||||
|
||||
newSubplotName = '';
|
||||
|
||||
selAvailableIndicator = '';
|
||||
|
||||
selIndicator = '';
|
||||
|
||||
graphType = 'line';
|
||||
|
||||
availableGraphTypes = ['line', 'bar', 'scatter'];
|
||||
|
||||
showConfig = false;
|
||||
|
||||
selSubPlot = '';
|
||||
|
||||
tempPlotConfig?: PlotConfig = undefined;
|
||||
|
||||
tempPlotConfigValid = true;
|
||||
|
||||
selColor = randomColor();
|
||||
|
||||
@ftbot.Mutation saveCustomPlotConfig;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Mutation updatePlotConfigName!: (plotConfigName: string) => void;
|
||||
|
||||
@ftbot.State('plotConfigName') usedPlotConfigName!: string;
|
||||
|
||||
get plotConfigJson() {
|
||||
return JSON.stringify(this.plotConfig, null, 2);
|
||||
}
|
||||
|
||||
set plotConfigJson(newValue: string) {
|
||||
try {
|
||||
this.tempPlotConfig = JSON.parse(newValue);
|
||||
// TODO: Should Validate schema validity (should be PlotConfig type...)
|
||||
this.tempPlotConfigValid = true;
|
||||
} catch (err) {
|
||||
this.tempPlotConfigValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
get subplots() {
|
||||
// Subplot keys (for selection window)
|
||||
return Object.keys(this.plotConfig.subplots);
|
||||
}
|
||||
|
||||
get usedColumns() {
|
||||
if (this.isMainPlot) {
|
||||
return Object.keys(this.plotConfig.main_plot);
|
||||
}
|
||||
if (this.selSubPlot in this.plotConfig[this.plotOption]) {
|
||||
return Object.keys(this.plotConfig[this.plotOption][this.selSubPlot]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
get isMainPlot() {
|
||||
return this.plotOption === 'main_plot';
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.plotConfig = this.value;
|
||||
this.plotConfigName = this.usedPlotConfigName;
|
||||
}
|
||||
|
||||
newColor() {
|
||||
this.selColor = randomColor();
|
||||
}
|
||||
|
||||
addIndicator() {
|
||||
console.log(this.plotConfig);
|
||||
|
||||
const { plotConfig } = this;
|
||||
if (this.isMainPlot) {
|
||||
console.log(`Adding ${this.selAvailableIndicator} to MainPlot`);
|
||||
plotConfig[this.plotOption][this.selAvailableIndicator] = {
|
||||
color: this.selColor,
|
||||
type: this.graphType,
|
||||
};
|
||||
} else {
|
||||
console.log(`Adding ${this.selAvailableIndicator} to ${this.selSubPlot}`);
|
||||
plotConfig[this.plotOption][this.selSubPlot][this.selAvailableIndicator] = {
|
||||
color: this.selColor,
|
||||
type: this.graphType,
|
||||
};
|
||||
}
|
||||
|
||||
this.plotConfig = { ...plotConfig };
|
||||
this.selAvailableIndicator = '';
|
||||
// Reset random color
|
||||
this.newColor();
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
|
||||
removeIndicator() {
|
||||
console.log(this.plotConfig);
|
||||
const { plotConfig } = this;
|
||||
if (this.isMainPlot) {
|
||||
console.log(`Removing ${this.selIndicator} from MainPlot`);
|
||||
delete plotConfig[this.plotOption][this.selIndicator];
|
||||
} else {
|
||||
console.log(`Removing ${this.selIndicator} from ${this.selSubPlot}`);
|
||||
delete plotConfig[this.plotOption][this.selSubPlot][this.selIndicator];
|
||||
}
|
||||
|
||||
this.plotConfig = { ...plotConfig };
|
||||
console.log(this.plotConfig);
|
||||
this.selIndicator = '';
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
|
||||
addSubplot() {
|
||||
this.plotConfig.subplots = {
|
||||
...this.plotConfig.subplots,
|
||||
[this.newSubplotName]: {},
|
||||
};
|
||||
this.selSubPlot = this.newSubplotName;
|
||||
this.newSubplotName = '';
|
||||
console.log(this.plotConfig);
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
|
||||
delSubplot() {
|
||||
delete this.plotConfig.subplots[this.selSubPlot];
|
||||
this.plotConfig.subplots = { ...this.plotConfig.subplots };
|
||||
}
|
||||
|
||||
savePlotConfig() {
|
||||
this.saveCustomPlotConfig({ [this.plotConfigName]: this.plotConfig });
|
||||
}
|
||||
|
||||
loadPlotConfig() {
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
|
||||
console.log(this.plotConfig);
|
||||
console.log('loading config');
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
|
||||
loadConfigFromString() {
|
||||
// this.plotConfig = JSON.parse();
|
||||
if (this.tempPlotConfig !== undefined && this.tempPlotConfigValid) {
|
||||
this.plotConfig = this.tempPlotConfig;
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
}
|
||||
|
||||
async loadPlotConfigFromStrategy() {
|
||||
await this.getStrategyPlotConfig();
|
||||
this.plotConfig = this.strategyPlotConfig;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.textArea {
|
||||
min-height: 250px;
|
||||
}
|
||||
.colorbox {
|
||||
border-radius: 50%;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
vertical-align: center;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
49
src/components/ftbot/StrategyList.vue
Normal file
49
src/components/ftbot/StrategyList.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<b-form-group label="Strategy" label-for="strategyName" invalid-feedback="Strategy is required">
|
||||
<b-form-select v-model="strategy" :options="strategyList" @change="strategyChanged">
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({})
|
||||
export default class StrategyList extends Vue {
|
||||
@Prop() value!: string;
|
||||
|
||||
@ftbot.Action getStrategyList;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action getStrategy!: (strategy: string) => void;
|
||||
|
||||
@ftbot.State strategyList;
|
||||
|
||||
@Emit('input')
|
||||
emitStrategy(strategy: string) {
|
||||
this.getStrategy(strategy);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
get strategy() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set strategy(val) {
|
||||
this.emitStrategy(val);
|
||||
}
|
||||
|
||||
strategyChanged(newVal) {
|
||||
this.value = newVal;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.getStrategyList();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
79
src/components/ftbot/TimeRangeSelect.vue
Normal file
79
src/components/ftbot/TimeRangeSelect.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<b-card class="row">
|
||||
<b-list-group class="col-mb-4" horizontal="md">
|
||||
<b-form-group label="Start date" label-for="dp_dateFrom">
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<b-form-datepicker v-model="dateFrom" class="mb-2" button-only></b-form-datepicker>
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
id="dp_dateFrom"
|
||||
v-model="dateFrom"
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
autocomplete="off"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<b-form-group class="ml-2" label="End date" label-for="dp_dateTo">
|
||||
<b-input-group>
|
||||
<b-input-group-prepend>
|
||||
<b-form-datepicker v-model="dateTo" class="mb-2" button-only></b-form-datepicker>
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
id="dp_dateTo"
|
||||
v-model="dateTo"
|
||||
type="text"
|
||||
placeholder="YYYY-MM-DD"
|
||||
autocomplete="off"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<label
|
||||
>Timerange: <b>{{ timeRange }}</b></label
|
||||
>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Emit } from 'vue-property-decorator';
|
||||
import { dateStringToTimeRange, timestampToDateString } from '@/shared/formatters';
|
||||
|
||||
const now = new Date();
|
||||
@Component({})
|
||||
export default class TimeRangeSelect extends Vue {
|
||||
dateFrom = timestampToDateString(new Date(now.getFullYear(), now.getMonth() - 1, 1));
|
||||
|
||||
dateTo = '';
|
||||
|
||||
@Emit('input')
|
||||
emitTimeRange() {
|
||||
return this.timeRange;
|
||||
}
|
||||
|
||||
created() {
|
||||
this.emitTimeRange();
|
||||
}
|
||||
|
||||
updated() {
|
||||
this.emitTimeRange();
|
||||
}
|
||||
|
||||
get timeRange() {
|
||||
if (this.dateFrom !== '' || this.dateTo !== '') {
|
||||
return `${dateStringToTimeRange(this.dateFrom)}-${dateStringToTimeRange(this.dateTo)}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-body {
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.8rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
</style>
|
|
@ -12,6 +12,7 @@
|
|||
<b-navbar-nav>
|
||||
<b-nav-item to="/trade">Trade</b-nav-item>
|
||||
<b-nav-item to="/dashboard">Dashboard</b-nav-item>
|
||||
<!-- <b-nav-item to="/graph">Graph</b-nav-item> -->
|
||||
<BootswatchThemeSelect />
|
||||
</b-navbar-nav>
|
||||
<!-- Right aligned nav items -->
|
||||
|
|
|
@ -24,6 +24,14 @@ const routes: Array<RouteConfig> = [
|
|||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '@/views/Trading.vue'),
|
||||
},
|
||||
{
|
||||
path: '/graph',
|
||||
name: 'Freqtrade Graph',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '@/views/Graphs.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Freqtrade Dashboard',
|
||||
|
|
|
@ -16,6 +16,22 @@ export function timestampms(ts: number | Date): string {
|
|||
return moment.utc(ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts timestamp or Date object to YYYY-MM-DD format.
|
||||
* @param ts
|
||||
*/
|
||||
export function timestampToDateString(ts: number | Date): string {
|
||||
return moment(ts).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a String of the format YYYY-MM-DD to YYYYMMDD. To be used as timerange.
|
||||
* @param datestring Input string (in the format YYYY-MM-DD)
|
||||
*/
|
||||
export function dateStringToTimeRange(datestring: string): string {
|
||||
return datestring.replace(/-/g, '');
|
||||
}
|
||||
|
||||
export function timestampHour(ts: number | Date): number {
|
||||
return moment.utc(ts).hour();
|
||||
}
|
||||
|
@ -24,4 +40,6 @@ export default {
|
|||
formatPrice,
|
||||
formatPercent,
|
||||
timestampms,
|
||||
timestampToDateString,
|
||||
dateStringToTimeRange,
|
||||
};
|
||||
|
|
4
src/shared/randomColor.ts
Normal file
4
src/shared/randomColor.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default function () {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return `#${((Math.random() * 0xffffff) << 0).toString(16)}`;
|
||||
}
|
35
src/shared/storage.ts
Normal file
35
src/shared/storage.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { PlotConfig, EMPTY_PLOTCONFIG, PlotConfigStorage } from '@/types';
|
||||
|
||||
const PLOT_CONFIG = 'ft_custom_plot_config';
|
||||
const PLOT_CONFIG_NAME = 'ft_selected_plot_config';
|
||||
|
||||
export function getPlotConfigName(): string {
|
||||
return localStorage.getItem(PLOT_CONFIG_NAME) || 'default';
|
||||
}
|
||||
|
||||
export function storePlotConfigName(plotConfigName: string): void {
|
||||
localStorage.setItem(PLOT_CONFIG_NAME, plotConfigName);
|
||||
}
|
||||
|
||||
export function getAllCustomPlotConfig(): PlotConfig {
|
||||
return JSON.parse(localStorage.getItem(PLOT_CONFIG) || '{}');
|
||||
}
|
||||
|
||||
export function getAllPlotConfigNames(): Array<string> {
|
||||
return Object.keys(getAllCustomPlotConfig());
|
||||
}
|
||||
|
||||
export function getCustomPlotConfig(configName: string): PlotConfig {
|
||||
const configs = getAllCustomPlotConfig();
|
||||
return configName in configs ? configs[configName] : { ...EMPTY_PLOTCONFIG };
|
||||
}
|
||||
|
||||
export function storeCustomPlotConfig(plotConfig: PlotConfigStorage) {
|
||||
const existingConfig = getAllCustomPlotConfig();
|
||||
// Merge existing with new config
|
||||
const finalPlotConfig = { ...existingConfig, ...plotConfig };
|
||||
|
||||
localStorage.setItem(PLOT_CONFIG, JSON.stringify(finalPlotConfig));
|
||||
// Store new config name as default
|
||||
storePlotConfigName(Object.keys(plotConfig)[0]);
|
||||
}
|
17
src/shared/timemath.ts
Normal file
17
src/shared/timemath.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
const ROUND_UP = 2;
|
||||
const ROUND_DOWN = 3;
|
||||
|
||||
export function roundTimeframe(
|
||||
timeframems: number,
|
||||
timestamp: number,
|
||||
direction: number = ROUND_DOWN,
|
||||
) {
|
||||
const offset = timestamp % timeframems;
|
||||
return timestamp - offset + (direction === ROUND_UP ? timeframems : 0);
|
||||
}
|
||||
|
||||
export default {
|
||||
ROUND_UP,
|
||||
ROUND_DOWN,
|
||||
roundTimeframe,
|
||||
};
|
|
@ -1,12 +1,38 @@
|
|||
import { api } from '@/shared/apiService';
|
||||
import { BotState, BlacklistPayload, ForcebuyPayload, Logs, DailyPayload, Trade } from '@/types';
|
||||
import {
|
||||
BotState,
|
||||
BlacklistPayload,
|
||||
ForcebuyPayload,
|
||||
Logs,
|
||||
DailyPayload,
|
||||
Trade,
|
||||
PairCandlePayload,
|
||||
PairHistoryPayload,
|
||||
PlotConfig,
|
||||
StrategyListResult,
|
||||
EMPTY_PLOTCONFIG,
|
||||
AvailablePairPayload,
|
||||
PlotConfigStorage,
|
||||
WhitelistResponse,
|
||||
StrategyResult,
|
||||
} from '@/types';
|
||||
|
||||
import {
|
||||
storeCustomPlotConfig,
|
||||
getPlotConfigName,
|
||||
getAllPlotConfigNames,
|
||||
storePlotConfigName,
|
||||
} from '@/shared/storage';
|
||||
import { showAlert } from './alerts';
|
||||
|
||||
export enum BotStoreGetters {
|
||||
openTrades = 'openTrades',
|
||||
tradeDetail = 'tradeDetail',
|
||||
closedTrades = 'closedTrades',
|
||||
allTrades = 'allTrades',
|
||||
plotConfig = 'plotConfig',
|
||||
plotConfigNames = 'plotConfigNames',
|
||||
timeframe = 'timeframe',
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -26,11 +52,29 @@ export default {
|
|||
dailyStats: [],
|
||||
pairlistMethods: [],
|
||||
detailTradeId: null,
|
||||
candleData: {},
|
||||
history: {},
|
||||
strategyPlotConfig: {},
|
||||
customPlotConfig: { ...EMPTY_PLOTCONFIG },
|
||||
plotConfigName: getPlotConfigName(),
|
||||
availablePlotConfigNames: getAllPlotConfigNames(),
|
||||
strategyList: [],
|
||||
strategy: {},
|
||||
pairlist: [],
|
||||
},
|
||||
getters: {
|
||||
[BotStoreGetters.plotConfig](state) {
|
||||
return state.customPlotConfig[state.plotConfigName] || { ...EMPTY_PLOTCONFIG };
|
||||
},
|
||||
[BotStoreGetters.plotConfigNames](state): Array<string> {
|
||||
return Object.keys(state.customPlotConfig);
|
||||
},
|
||||
[BotStoreGetters.openTrades](state) {
|
||||
return state.openTrades;
|
||||
},
|
||||
[BotStoreGetters.allTrades](state) {
|
||||
return [...state.openTrades, ...state.trades];
|
||||
},
|
||||
[BotStoreGetters.tradeDetail](state) {
|
||||
let dTrade = state.openTrades.find((item) => item.trade_id === state.detailTradeId);
|
||||
if (!dTrade) {
|
||||
|
@ -41,6 +85,9 @@ export default {
|
|||
[BotStoreGetters.closedTrades](state) {
|
||||
return state.trades.filter((item) => !item.is_open);
|
||||
},
|
||||
[BotStoreGetters.timeframe](state) {
|
||||
return state.botState?.timeframe;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
updateTrades(state, trades) {
|
||||
|
@ -53,7 +100,7 @@ export default {
|
|||
updatePerformance(state, performance) {
|
||||
state.performanceStats = performance;
|
||||
},
|
||||
updateWhitelist(state, whitelist) {
|
||||
updateWhitelist(state, whitelist: WhitelistResponse) {
|
||||
state.whitelist = whitelist.whitelist;
|
||||
state.pairlistMethods = whitelist.method;
|
||||
},
|
||||
|
@ -81,6 +128,35 @@ export default {
|
|||
setDetailTrade(state, trade: Trade) {
|
||||
state.detailTradeId = trade ? trade.trade_id : null;
|
||||
},
|
||||
updateStrategyList(state, result: StrategyListResult) {
|
||||
state.strategyList = result.strategies;
|
||||
},
|
||||
updateStrategy(state, strategy: StrategyResult) {
|
||||
state.strategy = strategy;
|
||||
},
|
||||
updatePairs(state, pairlist: Array<string>) {
|
||||
state.pairlist = pairlist;
|
||||
},
|
||||
updatePairCandles(state, { pair, timeframe, data }) {
|
||||
state.candleData = { ...state.candleData, [`${pair}__${timeframe}`]: data };
|
||||
},
|
||||
updatePairHistory(state, { pair, timeframe, data }) {
|
||||
// Intentionally drop the previous state here.
|
||||
state.history = { [`${pair}__${timeframe}`]: data };
|
||||
},
|
||||
updatePlotConfig(state, plotConfig: PlotConfig) {
|
||||
state.strategyPlotConfig = plotConfig;
|
||||
},
|
||||
updatePlotConfigName(state, plotConfigName: string) {
|
||||
// Set default plot config name
|
||||
state.plotConfigName = plotConfigName;
|
||||
storePlotConfigName(plotConfigName);
|
||||
},
|
||||
saveCustomPlotConfig(state, plotConfig: PlotConfigStorage) {
|
||||
state.customPlotConfig = plotConfig;
|
||||
storeCustomPlotConfig(plotConfig);
|
||||
state.availablePlotConfigNames = getAllPlotConfigNames();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
ping({ commit, rootState }) {
|
||||
|
@ -109,17 +185,112 @@ export default {
|
|||
.then((result) => commit('updateOpenTrades', result.data))
|
||||
.catch(console.error);
|
||||
},
|
||||
getPerformance({ commit }) {
|
||||
getPairCandles({ commit }, payload: PairCandlePayload) {
|
||||
if (payload.pair && payload.timeframe && payload.limit) {
|
||||
return api
|
||||
.get('/pair_candles', {
|
||||
params: { ...payload },
|
||||
})
|
||||
.then((result) => {
|
||||
commit('updatePairCandles', {
|
||||
pair: payload.pair,
|
||||
timeframe: payload.timeframe,
|
||||
data: result.data,
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'pair or timeframe not specified';
|
||||
console.error(error);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
getPairHistory({ commit }, payload: PairHistoryPayload) {
|
||||
if (payload.pair && payload.timeframe && payload.timerange) {
|
||||
return api
|
||||
.get('/pair_history', {
|
||||
params: { ...payload },
|
||||
timeout: 10000,
|
||||
})
|
||||
.then((result) => {
|
||||
commit('updatePairHistory', {
|
||||
pair: payload.pair,
|
||||
timeframe: payload.timeframe,
|
||||
timerange: payload.timerange,
|
||||
data: result.data,
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'pair or timeframe or timerange not specified';
|
||||
console.error(error);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
getStrategyPlotConfig({ commit }) {
|
||||
return api
|
||||
.get('/performance')
|
||||
.then((result) => commit('updatePerformance', result.data))
|
||||
.get('/plot_config')
|
||||
.then((result) => commit('updatePlotConfig', result.data))
|
||||
.catch(console.error);
|
||||
},
|
||||
setPlotConfigName({ commit }, plotConfigName: string) {
|
||||
commit('updatePlotConfigName', plotConfigName);
|
||||
},
|
||||
getStrategyList({ commit }) {
|
||||
return api
|
||||
.get('/strategies')
|
||||
.then((result) => commit('updateStrategyList', result.data))
|
||||
.catch(console.error);
|
||||
},
|
||||
async getStrategy({ commit }, strategy: string) {
|
||||
try {
|
||||
const result = await api.get(`/strategy/${strategy}`, {});
|
||||
commit('updateStrategy', result.data);
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async getAvailablePairs({ commit }, payload: AvailablePairPayload) {
|
||||
try {
|
||||
const result = await api.get('/available_pairs', {
|
||||
params: { ...payload },
|
||||
});
|
||||
// result is of type AvailablePairResult
|
||||
const { pairs } = result.data;
|
||||
commit('updatePairs', pairs);
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async getPerformance({ commit }) {
|
||||
try {
|
||||
const result = await api.get('/performance');
|
||||
commit('updatePerformance', result.data);
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
getWhitelist({ commit }) {
|
||||
return api
|
||||
.get('/whitelist')
|
||||
.then((result) => commit('updateWhitelist', result.data))
|
||||
.catch(console.error);
|
||||
.then((result) => {
|
||||
commit('updateWhitelist', result.data);
|
||||
return Promise.resolve(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
getBlacklist({ commit }) {
|
||||
return api
|
||||
|
|
|
@ -7,6 +7,7 @@ export enum TradeLayout {
|
|||
tradeHistory = 'g-tradeHistory',
|
||||
tradeDetail = 'g-tradeDetail',
|
||||
logView = 'g-logView',
|
||||
chartView = 'g-chartView',
|
||||
}
|
||||
|
||||
export enum DashboardLayout {
|
||||
|
@ -20,10 +21,11 @@ export enum DashboardLayout {
|
|||
const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
|
||||
{ i: TradeLayout.botControls, x: 0, y: 0, w: 4, h: 4 },
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 4, h: 7 },
|
||||
{ i: TradeLayout.openTrades, x: 4, y: 0, w: 8, h: 5 },
|
||||
{ i: TradeLayout.tradeDetail, x: 4, y: 4, w: 8, h: 5 },
|
||||
{ i: TradeLayout.tradeHistory, x: 4, y: 4, w: 8, h: 6 },
|
||||
{ i: TradeLayout.logView, x: 0, y: 9, w: 12, h: 3 },
|
||||
{ i: TradeLayout.chartView, x: 4, y: 0, w: 8, h: 11 },
|
||||
{ i: TradeLayout.tradeDetail, x: 0, y: 11, w: 5, h: 6 },
|
||||
{ i: TradeLayout.openTrades, x: 5, y: 11, w: 7, h: 5 },
|
||||
{ i: TradeLayout.tradeHistory, x: 5, y: 12, w: 7, h: 6 },
|
||||
{ i: TradeLayout.logView, x: 0, y: 16, w: 12, h: 3 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// variables created for the project and not overwrite of bootstrap
|
||||
|
||||
|
||||
$font-size-base: 0.9rem;
|
||||
$fontsize-small: 0.9rem;
|
||||
|
|
|
@ -8,3 +8,9 @@ export interface BlacklistResponse {
|
|||
blacklist: Array<string>;
|
||||
errors: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface WhitelistResponse {
|
||||
method: Array<string>;
|
||||
length: number;
|
||||
whitelist: Array<string>;
|
||||
}
|
||||
|
|
|
@ -2,5 +2,6 @@ export * from './auth';
|
|||
export * from './blacklist';
|
||||
export * from './chart';
|
||||
export * from './daily';
|
||||
export * from './plot';
|
||||
export * from './profit';
|
||||
export * from './types';
|
||||
|
|
16
src/types/plot.ts
Normal file
16
src/types/plot.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export interface IndicatorConfig {
|
||||
color?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface PlotConfig {
|
||||
main_plot: Record<string, IndicatorConfig>;
|
||||
subplots: Record<string, Record<string, IndicatorConfig>>;
|
||||
}
|
||||
|
||||
export interface PlotConfigStorage {
|
||||
[key: string]: PlotConfig;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
export const EMPTY_PLOTCONFIG: PlotConfig = { main_plot: {}, subplots: {} };
|
|
@ -56,8 +56,10 @@ export interface BotState {
|
|||
strategy: string;
|
||||
/** Timeframe in readable form (e.g. 5m) */
|
||||
timeframe: string;
|
||||
/** Timeframe in Milliseconds */
|
||||
/** Timeframe in milliseconds */
|
||||
timeframe_ms: number;
|
||||
/** Timeframe in Minutes */
|
||||
timeframe_min: number;
|
||||
|
||||
trailing_only_offset_is_reached: boolean;
|
||||
trailing_stop: boolean;
|
||||
|
@ -137,3 +139,64 @@ export interface ClosedTrade extends Trade {
|
|||
initial_stop_loss_pct?: number;
|
||||
open_order_id?: string;
|
||||
}
|
||||
|
||||
export interface StrategyListResult {
|
||||
strategies: Array<string>;
|
||||
}
|
||||
|
||||
export interface StrategyResult {
|
||||
/** Strategy name */
|
||||
strategy: string;
|
||||
/** Code of the strategy class */
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface AvailablePairPayload {
|
||||
timeframe?: string;
|
||||
stake_currency?: string;
|
||||
}
|
||||
|
||||
export interface AvailablePairResult {
|
||||
pairs: Array<string>;
|
||||
/**
|
||||
* List of lists, as [pair, timeframe]
|
||||
*/
|
||||
pair_interval: Array<Array<string>>;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface PairCandlePayload {
|
||||
pair: string;
|
||||
timeframe: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface PairHistoryPayload {
|
||||
pair: string;
|
||||
timeframe: string;
|
||||
timerange: string;
|
||||
strategy: string;
|
||||
}
|
||||
|
||||
export interface PairHistory {
|
||||
strategy: string;
|
||||
pair: string;
|
||||
timeframe: string;
|
||||
timeframe_ms: number;
|
||||
columns: string[];
|
||||
data: number[];
|
||||
length: number;
|
||||
/** Number of buy signals in this response */
|
||||
buy_signals: number;
|
||||
/** Number of sell signals in this response */
|
||||
sell_signals: number;
|
||||
last_analyzed: number;
|
||||
/** Data start date in as millisecond timestamp */
|
||||
data_start_ts: number;
|
||||
/** Data start date in in the format YYYY-MM-DD HH24:MI:SS+00:00 */
|
||||
data_start: string;
|
||||
/** End date in in the format YYYY-MM-DD HH24:MI:SS+00:00 */
|
||||
data_stop: string;
|
||||
/** Data end date in as millisecond timestamp */
|
||||
data_stop_ts: number;
|
||||
}
|
||||
|
|
77
src/views/Graphs.vue
Normal file
77
src/views/Graphs.vue
Normal file
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-mb-2">
|
||||
<b-checkbox v-model="historicView">HistoricData</b-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="historicView" class="mt-2 row">
|
||||
<TimeRangeSelect v-model="timerange" class="col-md-4 mr-2"></TimeRangeSelect>
|
||||
<StrategyList v-model="strategy" class="col-md-2"></StrategyList>
|
||||
</div>
|
||||
|
||||
<div class="row chart-row">
|
||||
<CandleChartContainer
|
||||
:available-pairs="historicView ? pairlist : whitelist"
|
||||
:historic-view="historicView"
|
||||
:timeframe="timeframe"
|
||||
:trades="trades"
|
||||
:timerange="historicView ? timerange : ''"
|
||||
:strategy="historicView ? strategy : ''"
|
||||
>
|
||||
</CandleChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||
import TimeRangeSelect from '@/components/ftbot/TimeRangeSelect.vue';
|
||||
import StrategyList from '@/components/ftbot/StrategyList.vue';
|
||||
import { AvailablePairPayload, AvailablePairResult, WhitelistResponse } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({
|
||||
components: { CandleChartContainer, StrategyList, TimeRangeSelect },
|
||||
})
|
||||
export default class Graphs extends Vue {
|
||||
historicView = false;
|
||||
|
||||
strategy = '';
|
||||
|
||||
timerange = '';
|
||||
|
||||
@ftbot.State pairlist;
|
||||
|
||||
@ftbot.State whitelist;
|
||||
|
||||
@ftbot.State trades;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.timeframe]!: string;
|
||||
|
||||
@ftbot.Action public getWhitelist!: () => Promise<WhitelistResponse>;
|
||||
|
||||
@ftbot.Action public getAvailablePairs!: (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
payload: AvailablePairPayload,
|
||||
) => Promise<AvailablePairResult>;
|
||||
|
||||
mounted() {
|
||||
this.getWhitelist();
|
||||
// this.refresh();
|
||||
this.getAvailablePairs({ timeframe: this.timeframe }).then((val) => {
|
||||
console.log(val);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-row {
|
||||
height: 820px;
|
||||
}
|
||||
</style>
|
|
@ -90,6 +90,7 @@
|
|||
:y="gridLayoutTradeDetail.y"
|
||||
:w="gridLayoutTradeDetail.w"
|
||||
:h="gridLayoutTradeDetail.h"
|
||||
:min-h="4"
|
||||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Trade Detail">
|
||||
|
@ -108,6 +109,25 @@
|
|||
<LogViewer />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutChartView.i"
|
||||
:x="gridLayoutChartView.x"
|
||||
:y="gridLayoutChartView.y"
|
||||
:w="gridLayoutChartView.w"
|
||||
:h="gridLayoutChartView.h"
|
||||
:min-h="6"
|
||||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Chart">
|
||||
<CandleChartContainer
|
||||
:available-pairs="whitelist"
|
||||
:historic-view="!!false"
|
||||
:timeframe="timeframe"
|
||||
:trades="allTrades"
|
||||
>
|
||||
</CandleChartContainer>
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
</GridLayout>
|
||||
</template>
|
||||
|
||||
|
@ -127,6 +147,7 @@ import TradeDetail from '@/components/ftbot/TradeDetail.vue';
|
|||
import ReloadControl from '@/components/ftbot/ReloadControl.vue';
|
||||
import LogViewer from '@/components/ftbot/LogViewer.vue';
|
||||
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||
|
||||
import { Trade } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
|
@ -150,17 +171,24 @@ const layoutNs = namespace('layout');
|
|||
TradeDetail,
|
||||
ReloadControl,
|
||||
LogViewer,
|
||||
CandleChartContainer,
|
||||
},
|
||||
})
|
||||
export default class Trading extends Vue {
|
||||
@ftbot.State detailTradeId!: number;
|
||||
|
||||
@ftbot.Getter openTrades!: Trade[];
|
||||
@ftbot.Getter [BotStoreGetters.openTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter closedTrades!: Trade[];
|
||||
@ftbot.Getter [BotStoreGetters.closedTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.allTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.tradeDetail]!: Trade;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.timeframe]!: string;
|
||||
|
||||
@ftbot.State whitelist!: string[];
|
||||
|
||||
@layoutNs.Getter getTradingLayout!: GridItemData[];
|
||||
|
||||
@layoutNs.Mutation setTradingLayout;
|
||||
|
@ -193,6 +221,10 @@ export default class Trading extends Vue {
|
|||
return findGridLayout(this.gridLayout, TradeLayout.logView);
|
||||
}
|
||||
|
||||
get gridLayoutChartView(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.chartView);
|
||||
}
|
||||
|
||||
layoutUpdatedEvent(newLayout) {
|
||||
this.setTradingLayout(newLayout);
|
||||
}
|
||||
|
|
260
yarn.lock
260
yarn.lock
|
@ -893,10 +893,10 @@
|
|||
dependencies:
|
||||
"@hapi/hoek" "^8.3.0"
|
||||
|
||||
"@interactjs/types@1.9.22":
|
||||
version "1.9.22"
|
||||
resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.9.22.tgz#1504d3170b062555b03ee8eb954ae653b7f614e2"
|
||||
integrity sha512-GMMMCYE+FPrKCOOOqQ/ImpqLyinb6e8psw9MR9ymTJxnkmJMKrY/GDC/187PVxpWdtSqW+GibkeRfUCOv6vFjg==
|
||||
"@interactjs/types@1.10.0":
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.0.tgz#c2232ed3ba503891349912fa74aa05aa467403e9"
|
||||
integrity sha512-TB4uHd++aXqiWHQNzERcP8cxOLHCd+b3fq0XoV7Ydnappoq25R8iIrrB8Yb2FqVkZXnj1SIFYnzpol1SYHFIMQ==
|
||||
|
||||
"@intervolga/optimize-cssnano-plugin@^1.0.5":
|
||||
version "1.0.6"
|
||||
|
@ -997,10 +997,17 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/echarts@^4.6.2":
|
||||
version "4.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/echarts/-/echarts-4.6.5.tgz#6aba35e7d5fdb97f0852865a11ad1cd52f0b72fe"
|
||||
integrity sha512-lzYceya5tCBAUTjYnTP2Lwd1VAlyjLfWm3pRFqS4Nzj+Lb+1ej+uX40miM/je73jqVXvO+g3FTMNzyKWDmLR1Q==
|
||||
dependencies:
|
||||
"@types/zrender" "*"
|
||||
|
||||
"@types/express-serve-static-core@*":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz#9a487da757425e4f267e7d1c5720226af7f89591"
|
||||
integrity sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA==
|
||||
version "4.17.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084"
|
||||
integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
|
@ -1055,13 +1062,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
|
||||
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
|
||||
|
||||
"@types/mini-css-extract-plugin@^0.9.1":
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.1.tgz#d4bdde5197326fca039d418f4bdda03dc74dc451"
|
||||
integrity sha512-+mN04Oszdz9tGjUP/c1ReVwJXxSniLd7lF++sv+8dkABxVNthg6uccei+4ssKxRHGoMmPxdn7uBdJWONSJGTGQ==
|
||||
dependencies:
|
||||
"@types/webpack" "*"
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
|
@ -1073,9 +1073,9 @@
|
|||
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
|
||||
|
||||
"@types/node@*":
|
||||
version "14.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.1.tgz#56af902ad157e763f9ba63d671c39cda3193c835"
|
||||
integrity sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw==
|
||||
version "14.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
|
||||
integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
|
@ -1121,9 +1121,9 @@
|
|||
integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b"
|
||||
integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w==
|
||||
version "3.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.11.0.tgz#2868d405cc45cd9dc3069179052103032c33afbc"
|
||||
integrity sha512-I0Yd8TUELTbgRHq2K65j8rnDPAzAP+DiaF/syLem7yXwYLsHZhPd+AM2iXsWmf9P2F2NlFCgl5erZPQx9IbM9Q==
|
||||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
|
@ -1144,9 +1144,9 @@
|
|||
integrity sha512-5oiXqR7kwDGZ6+gmzIO2lTC+QsriNuQXZDWNYRV3l2XRN/zmPgnC21DLSx2D05zvD8vnXW6qUg7JnXZ4I6qLVQ==
|
||||
|
||||
"@types/webpack-sources@*":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.2.tgz#5d3d4dea04008a779a90135ff96fb5c0c9e6292c"
|
||||
integrity sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw==
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.0.0.tgz#08216ab9be2be2e1499beaebc4d469cec81e82a7"
|
||||
integrity sha512-a5kPx98CNFRKQ+wqawroFunvFqv7GHm/3KOI52NY9xWADgc8smu4R6prt4EU/M4QfVjvgBkMqU4fBhw3QfMVkg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/source-list-map" "*"
|
||||
|
@ -1164,6 +1164,11 @@
|
|||
"@types/webpack-sources" "*"
|
||||
source-map "^0.6.0"
|
||||
|
||||
"@types/zrender@*":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/zrender/-/zrender-4.0.0.tgz#a6806f12ec4eccaaebd9b0d816f049aca6188fbd"
|
||||
integrity sha512-s89GOIeKFiod2KSqHkfd2rzx+T2DVu7ihZCBEBnhFrzvQPUmzvDSBot9Fi1DfMQm9Odg+rTqoMGC38RvrwJK2w==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^2.33.0":
|
||||
version "2.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9"
|
||||
|
@ -1185,27 +1190,27 @@
|
|||
eslint-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/parser@^4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.2.0.tgz#1879ef400abd73d972e20f14c3522e5b343d1d1b"
|
||||
integrity sha512-54jJ6MwkOtowpE48C0QJF9iTz2/NZxfKVJzv1ha5imigzHbNSLN9yvbxFFH1KdlRPQrlR8qxqyOvLHHxd397VA==
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.3.0.tgz#684fc0be6551a2bfcb253991eec3c786a8c063a3"
|
||||
integrity sha512-JyfRnd72qRuUwItDZ00JNowsSlpQGeKfl9jxwO0FHK1qQ7FbYdoy5S7P+5wh1ISkT2QyAvr2pc9dAemDxzt75g==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "4.2.0"
|
||||
"@typescript-eslint/types" "4.2.0"
|
||||
"@typescript-eslint/typescript-estree" "4.2.0"
|
||||
"@typescript-eslint/scope-manager" "4.3.0"
|
||||
"@typescript-eslint/types" "4.3.0"
|
||||
"@typescript-eslint/typescript-estree" "4.3.0"
|
||||
debug "^4.1.1"
|
||||
|
||||
"@typescript-eslint/scope-manager@4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz#d10e6854a65e175b22a28265d372a97c8cce4bfc"
|
||||
integrity sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==
|
||||
"@typescript-eslint/scope-manager@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz#c743227e087545968080d2362cfb1273842cb6a7"
|
||||
integrity sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.2.0"
|
||||
"@typescript-eslint/visitor-keys" "4.2.0"
|
||||
"@typescript-eslint/types" "4.3.0"
|
||||
"@typescript-eslint/visitor-keys" "4.3.0"
|
||||
|
||||
"@typescript-eslint/types@4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.2.0.tgz#6f6b094329e72040f173123832397c7c0b910fc8"
|
||||
integrity sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==
|
||||
"@typescript-eslint/types@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf"
|
||||
integrity sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@2.34.0":
|
||||
version "2.34.0"
|
||||
|
@ -1220,13 +1225,13 @@
|
|||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/typescript-estree@4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz#9d746240991c305bf225ad5e96cbf57e7fea0551"
|
||||
integrity sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==
|
||||
"@typescript-eslint/typescript-estree@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz#0edc1068e6b2e4c7fdc54d61e329fce76241cee8"
|
||||
integrity sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.2.0"
|
||||
"@typescript-eslint/visitor-keys" "4.2.0"
|
||||
"@typescript-eslint/types" "4.3.0"
|
||||
"@typescript-eslint/visitor-keys" "4.3.0"
|
||||
debug "^4.1.1"
|
||||
globby "^11.0.1"
|
||||
is-glob "^4.0.1"
|
||||
|
@ -1234,12 +1239,12 @@
|
|||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz#ae13838e3a260b63ae51021ecaf1d0cdea8dbba5"
|
||||
integrity sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==
|
||||
"@typescript-eslint/visitor-keys@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0"
|
||||
integrity sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.2.0"
|
||||
"@typescript-eslint/types" "4.3.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
|
||||
|
@ -2298,12 +2303,12 @@ browserify-zlib@^0.2.0:
|
|||
pako "~1.0.5"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5:
|
||||
version "4.14.3"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.3.tgz#381f9e7f13794b2eb17e1761b4f118e8ae665a53"
|
||||
integrity sha512-GcZPC5+YqyPO4SFnz48/B0YaCwS47Q9iPChRGi6t7HhflKBcINzFrJvRfC+jp30sRMKxF+d4EHGs27Z0XP1NaQ==
|
||||
version "4.14.5"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015"
|
||||
integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001131"
|
||||
electron-to-chromium "^1.3.570"
|
||||
caniuse-lite "^1.0.30001135"
|
||||
electron-to-chromium "^1.3.571"
|
||||
escalade "^3.1.0"
|
||||
node-releases "^1.1.61"
|
||||
|
||||
|
@ -2485,10 +2490,10 @@ caniuse-api@^3.0.0:
|
|||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001131:
|
||||
version "1.0.30001133"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001133.tgz#ec564c5495311299eb05245e252d589a84acd95e"
|
||||
integrity sha512-s3XAUFaC/ntDb1O3lcw9K8MPeOW7KO3z9+GzAoBxfz1B0VdacXPMKgFUtG4KIsgmnbexmi013s9miVu4h+qMHw==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
|
||||
version "1.0.30001142"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001142.tgz#a8518fdb5fee03ad95ac9f32a9a1e5999469c250"
|
||||
integrity sha512-pDPpn9ankEpBFZXyCv2I4lh1v/ju+bqb78QfKf+w9XgDAFWBwSYPswXqprRdrgQWK0wQnpIbfwRjNHO1HWqvoQ==
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.3.0:
|
||||
version "2.3.0"
|
||||
|
@ -3120,9 +3125,9 @@ css-what@2.1:
|
|||
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
||||
|
||||
css-what@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39"
|
||||
integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e"
|
||||
integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g==
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
@ -3549,10 +3554,10 @@ ejs@^2.6.1:
|
|||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
|
||||
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
|
||||
|
||||
electron-to-chromium@^1.3.570:
|
||||
version "1.3.570"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.570.tgz#3f5141cc39b4e3892a276b4889980dabf1d29c7f"
|
||||
integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg==
|
||||
electron-to-chromium@^1.3.571:
|
||||
version "1.3.576"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz#2e70234484e03d7c7e90310d7d79fd3775379c34"
|
||||
integrity sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew==
|
||||
|
||||
element-resize-detector@^1.1.15:
|
||||
version "1.2.1"
|
||||
|
@ -3656,37 +3661,37 @@ error-stack-parser@^2.0.0:
|
|||
stackframe "^1.1.1"
|
||||
|
||||
es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
|
||||
version "1.17.6"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
|
||||
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
|
||||
version "1.17.7"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
|
||||
integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.2.0"
|
||||
is-regex "^1.1.0"
|
||||
object-inspect "^1.7.0"
|
||||
is-callable "^1.2.2"
|
||||
is-regex "^1.1.1"
|
||||
object-inspect "^1.8.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
object.assign "^4.1.1"
|
||||
string.prototype.trimend "^1.0.1"
|
||||
string.prototype.trimstart "^1.0.1"
|
||||
|
||||
es-abstract@^1.18.0-next.0:
|
||||
version "1.18.0-next.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
|
||||
integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
|
||||
es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
|
||||
version "1.18.0-next.1"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
|
||||
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.2.0"
|
||||
is-callable "^1.2.2"
|
||||
is-negative-zero "^2.0.0"
|
||||
is-regex "^1.1.1"
|
||||
object-inspect "^1.8.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
object.assign "^4.1.1"
|
||||
string.prototype.trimend "^1.0.1"
|
||||
string.prototype.trimstart "^1.0.1"
|
||||
|
||||
|
@ -3733,13 +3738,13 @@ eslint-config-airbnb@^18.2.0:
|
|||
object.entries "^1.1.2"
|
||||
|
||||
eslint-config-prettier@^6.0.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1"
|
||||
integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2"
|
||||
integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==
|
||||
dependencies:
|
||||
get-stdin "^6.0.0"
|
||||
|
||||
eslint-import-resolver-node@^0.3.3, eslint-import-resolver-node@^0.3.4:
|
||||
eslint-import-resolver-node@^0.3.4:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
|
||||
integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
|
||||
|
@ -3783,16 +3788,16 @@ eslint-module-utils@^2.6.0:
|
|||
pkg-dir "^2.0.0"
|
||||
|
||||
eslint-plugin-import@^2.21.2:
|
||||
version "2.22.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e"
|
||||
integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==
|
||||
version "2.22.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702"
|
||||
integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==
|
||||
dependencies:
|
||||
array-includes "^3.1.1"
|
||||
array.prototype.flat "^1.2.3"
|
||||
contains-path "^0.1.0"
|
||||
debug "^2.6.9"
|
||||
doctrine "1.5.0"
|
||||
eslint-import-resolver-node "^0.3.3"
|
||||
eslint-import-resolver-node "^0.3.4"
|
||||
eslint-module-utils "^2.6.0"
|
||||
has "^1.0.3"
|
||||
minimatch "^3.0.4"
|
||||
|
@ -5075,12 +5080,12 @@ inquirer@^7.0.0, inquirer@^7.1.0:
|
|||
strip-ansi "^6.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
interactjs@^1.6.3:
|
||||
version "1.9.22"
|
||||
resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.9.22.tgz#5ae98d2dd46d720cccdb89222b4e1ddcbe5cfeb9"
|
||||
integrity sha512-zUQefYtYJTazWKqDCSYV0vMJPFWp/PKXwpA3v75fD3+4+4J3/ItjlO7K3L1CpNWYU6s8uoEmwwOD6uDy6OoI/w==
|
||||
interactjs@^1.9.22:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.0.tgz#0fafd725b1fddfc9671cacd98c4f92ceae73a197"
|
||||
integrity sha512-dblDEizs758xETNVoSSxcZiwrP0DmFeNKILeDfhmO13LQRIsnSvbJFEYerEBVeH5m9qsOyYeMgBTafA+ZKkyBg==
|
||||
dependencies:
|
||||
"@interactjs/types" "1.9.22"
|
||||
"@interactjs/types" "1.10.0"
|
||||
|
||||
internal-ip@^4.3.0:
|
||||
version "4.3.0"
|
||||
|
@ -5175,10 +5180,10 @@ is-buffer@^1.1.5:
|
|||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
|
||||
is-callable@^1.1.4, is-callable@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
|
||||
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
|
||||
is-callable@^1.1.4, is-callable@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
|
||||
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
|
||||
|
||||
is-ci@^1.0.10:
|
||||
version "1.2.1"
|
||||
|
@ -5340,7 +5345,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
|||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
is-regex@^1.0.4, is-regex@^1.1.0, is-regex@^1.1.1:
|
||||
is-regex@^1.0.4, is-regex@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
|
||||
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
|
||||
|
@ -5936,11 +5941,16 @@ miller-rabin@^4.0.0:
|
|||
bn.js "^4.0.0"
|
||||
brorand "^1.0.1"
|
||||
|
||||
mime-db@1.44.0, "mime-db@>= 1.43.0 < 2":
|
||||
mime-db@1.44.0:
|
||||
version "1.44.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
|
||||
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
|
||||
|
||||
"mime-db@>= 1.43.0 < 2":
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
|
||||
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
|
||||
version "2.1.27"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
|
||||
|
@ -6322,18 +6332,18 @@ object-hash@^1.1.4:
|
|||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
|
||||
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
|
||||
|
||||
object-inspect@^1.7.0, object-inspect@^1.8.0:
|
||||
object-inspect@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
|
||||
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
|
||||
|
||||
object-is@^1.0.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
|
||||
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
|
||||
integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.5"
|
||||
es-abstract "^1.18.0-next.1"
|
||||
|
||||
object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
|
@ -6347,7 +6357,7 @@ object-visit@^1.0.0:
|
|||
dependencies:
|
||||
isobject "^3.0.0"
|
||||
|
||||
object.assign@^4.1.0:
|
||||
object.assign@^4.1.0, object.assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
|
||||
integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
|
||||
|
@ -6889,9 +6899,9 @@ postcss-discard-overridden@^4.0.1:
|
|||
postcss "^7.0.0"
|
||||
|
||||
postcss-load-config@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.1.tgz#0a684bb8beb05e55baf922f7ab44c3edb17cf78e"
|
||||
integrity sha512-D2ENobdoZsW0+BHy4x1CAkXtbXtYWYRIxL/JbtRBqrRGOPtJ2zoga/bEZWhV/ShWB5saVxJMzbMdSyA/vv4tXw==
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a"
|
||||
integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==
|
||||
dependencies:
|
||||
cosmiconfig "^5.0.0"
|
||||
import-cwd "^2.0.0"
|
||||
|
@ -7121,13 +7131,14 @@ postcss-selector-parser@^3.0.0:
|
|||
uniq "^1.0.1"
|
||||
|
||||
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
||||
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
|
||||
integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
indexes-of "^1.0.1"
|
||||
uniq "^1.0.1"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-svgo@^4.0.2:
|
||||
version "4.0.2"
|
||||
|
@ -7159,9 +7170,9 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
|
|||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||
|
||||
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
|
||||
version "7.0.34"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.34.tgz#f2baf57c36010df7de4009940f21532c16d65c20"
|
||||
integrity sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==
|
||||
version "7.0.35"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
|
||||
integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
source-map "^0.6.1"
|
||||
|
@ -8829,7 +8840,7 @@ use@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
@ -8955,12 +8966,12 @@ vue-functional-data-merge@^3.1.0:
|
|||
integrity sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA==
|
||||
|
||||
vue-grid-layout@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/vue-grid-layout/-/vue-grid-layout-2.3.8.tgz#47e4a8c16df9e3c917f8c142f86fb70594e86b3c"
|
||||
integrity sha512-bwvJo1BWe547w2Y9JhiwUGQr4YSSwSWKry4gClwnmkpWF4lKpJflHPvl0LGGknmmnpdEFOKdgreIcilh6Ry43w==
|
||||
version "2.3.9"
|
||||
resolved "https://registry.yarnpkg.com/vue-grid-layout/-/vue-grid-layout-2.3.9.tgz#c267e3fe3a7c06234b45c958960c2eca6ee6a4c2"
|
||||
integrity sha512-b6yofkSj+utfbh5mP1c0wLbcGWHT8pU2g4oRcd7jsTukKLyVBcPk7t5wJ3oyJ6erDOvAkIdIZP08RldHoQpiSA==
|
||||
dependencies:
|
||||
element-resize-detector "^1.1.15"
|
||||
interactjs "^1.6.3"
|
||||
interactjs "^1.9.22"
|
||||
|
||||
vue-hot-reload-api@^2.3.0:
|
||||
version "2.3.4"
|
||||
|
@ -8968,16 +8979,13 @@ vue-hot-reload-api@^2.3.0:
|
|||
integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==
|
||||
|
||||
"vue-loader-v16@npm:vue-loader@^16.0.0-beta.7":
|
||||
version "16.0.0-beta.7"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.0.0-beta.7.tgz#6f2726fa0e2b1fbae67895c47593bbf69f2b9ab8"
|
||||
integrity sha512-xQ8/GZmRPdQ3EinnE0IXwdVoDzh7Dowo0MowoyBuScEBXrRabw6At5/IdtD3waKklKW5PGokPsm8KRN6rvQ1cw==
|
||||
version "16.0.0-beta.8"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.0.0-beta.8.tgz#1f523d9fea8e8c6e4f5bb99fd768165af5845879"
|
||||
integrity sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==
|
||||
dependencies:
|
||||
"@types/mini-css-extract-plugin" "^0.9.1"
|
||||
chalk "^3.0.0"
|
||||
chalk "^4.1.0"
|
||||
hash-sum "^2.0.0"
|
||||
loader-utils "^1.2.3"
|
||||
merge-source-map "^1.1.0"
|
||||
source-map "^0.6.1"
|
||||
loader-utils "^2.0.0"
|
||||
|
||||
vue-loader@^15.9.2:
|
||||
version "15.9.3"
|
||||
|
|
Loading…
Reference in New Issue
Block a user