Merge pull request #106 from freqtrade/echarts_pairhistory

Echarts pairhistory
This commit is contained in:
Matthias 2020-10-04 19:00:42 +02:00 committed by GitHub
commit c53e0f89d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1771 additions and 143 deletions

View File

@ -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"
}
}

View 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>

View 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">&#x21bb;</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">&#9881;</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>

View 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"
>
&gt;
</b-button>
<b-button
variant="primary"
title="Remove indicator to plot"
size="sm"
:disabled="!selIndicator"
@click="removeIndicator"
>
&lt;
</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">&#x21bb;</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>

View 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>

View 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>

View File

@ -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 -->

View File

@ -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',

View File

@ -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,
};

View 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
View 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
View 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,
};

View File

@ -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

View File

@ -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[] = [

View File

@ -1,4 +1,3 @@
// variables created for the project and not overwrite of bootstrap
$font-size-base: 0.9rem;
$fontsize-small: 0.9rem;

View File

@ -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>;
}

View File

@ -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
View 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: {} };

View File

@ -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
View 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>

View File

@ -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
View File

@ -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"