composition: migrate remaining components

This commit is contained in:
Matthias 2022-04-21 06:37:57 +02:00
parent 2baabf7f04
commit e969cf83a8
6 changed files with 978 additions and 947 deletions

View File

@ -67,7 +67,7 @@ export default defineComponent({
get() { get() {
return botStore.botStores[props.bot.botId].autoRefresh; return botStore.botStores[props.bot.botId].autoRefresh;
}, },
set(_) { set() {
// pass // pass
}, },
}); });

View File

@ -5,7 +5,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
import { Trade, PairHistory, PlotConfig } from '@/types'; import { Trade, PairHistory, PlotConfig } from '@/types';
import randomColor from '@/shared/randomColor'; import randomColor from '@/shared/randomColor';
import { roundTimeframe } from '@/shared/timemath'; import { roundTimeframe } from '@/shared/timemath';
@ -71,85 +70,452 @@ const shortexitSignalColor = '#faba25';
const tradeBuyColor = 'cyan'; const tradeBuyColor = 'cyan';
const tradeSellColor = 'pink'; const tradeSellColor = 'pink';
@Component({ import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
export default defineComponent({
name: 'CandleChart',
components: { 'v-chart': ECharts }, components: { 'v-chart': ECharts },
}) props: {
export default class CandleChart extends Vue { trades: { required: false, default: () => [], type: Array as () => Trade[] },
$refs!: { dataset: { required: true, type: Object as () => PairHistory },
candleChart: typeof ECharts; heikinAshi: { required: false, default: false, type: Boolean },
useUTC: { required: false, default: true, type: Boolean },
plotConfig: { required: true, type: Object as () => PlotConfig },
theme: { default: 'dark', type: String },
},
setup(props) {
const candleChart = ref<typeof ECharts>();
const buyData = ref<number[][]>([]);
const sellData = ref<number[][]>([]);
const chartOptions = ref<EChartsOption>({});
const strategy = computed(() => {
return props.dataset ? props.dataset.strategy : '';
});
const pair = computed(() => {
return props.dataset ? props.dataset.pair : '';
});
const timeframe = computed(() => {
return props.dataset ? props.dataset.timeframe : '';
});
const timeframems = computed(() => {
return props.dataset ? props.dataset.timeframe_ms : 0;
});
const datasetColumns = computed(() => {
return props.dataset ? props.dataset.columns : [];
});
const hasData = computed(() => {
return props.dataset !== null && typeof props.dataset === 'object';
});
const filteredTrades = computed(() => {
return props.trades.filter((item: Trade) => item.pair === pair.value);
});
const chartTitle = computed(() => {
return `${strategy.value} - ${pair.value} - ${timeframe.value}`;
});
/** Return trade entries for charting */
const getTradeEntries = () => {
const trades: (string | number)[][] = [];
const tradesClose: (string | number)[][] = [];
for (let i = 0, len = filteredTrades.value.length; i < len; i += 1) {
const trade: Trade = filteredTrades.value[i];
if (
trade.open_timestamp >= props.dataset.data_start_ts &&
trade.open_timestamp <= props.dataset.data_stop_ts
) {
trades.push([roundTimeframe(timeframems.value, trade.open_timestamp), trade.open_rate]);
}
if (
trade.close_timestamp !== undefined &&
trade.close_timestamp < props.dataset.data_stop_ts &&
trade.close_timestamp > props.dataset.data_start_ts
) {
if (trade.close_date !== undefined && trade.close_rate !== undefined) {
tradesClose.push([
roundTimeframe(timeframems.value, trade.close_timestamp),
trade.close_rate,
]);
}
}
}
return { trades, tradesClose };
}; };
@Prop({ required: false, default: [] }) readonly trades!: Array<Trade>; const updateChart = (initial = false) => {
if (!hasData.value) {
return;
}
if (chartOptions.value?.title) {
chartOptions.value.title[0].text = chartTitle.value;
}
const colDate = props.dataset.columns.findIndex((el) => el === '__date_ts');
const colOpen = props.dataset.columns.findIndex((el) => el === 'open');
const colHigh = props.dataset.columns.findIndex((el) => el === 'high');
const colLow = props.dataset.columns.findIndex((el) => el === 'low');
const colClose = props.dataset.columns.findIndex((el) => el === 'close');
const colVolume = props.dataset.columns.findIndex((el) => el === 'volume');
// TODO: Remove below *signal_open after December 2021
const colBuyData = props.dataset.columns.findIndex(
(el) =>
el === '_buy_signal_open' ||
el === '_buy_signal_close' ||
el === '_enter_long_signal_close',
);
const colSellData = props.dataset.columns.findIndex(
(el) =>
el === '_sell_signal_open' ||
el === '_sell_signal_close' ||
el === '_exit_long_signal_close',
);
@Prop({ required: true }) readonly dataset!: PairHistory; const hasShorts =
props.dataset.enter_short_signals &&
props.dataset.enter_short_signals > 0 &&
props.dataset.exit_short_signals &&
props.dataset.exit_short_signals > 0;
const colShortEntryData = props.dataset.columns.findIndex(
(el) => el === '_enter_short_signal_close',
);
const colShortExitData = props.dataset.columns.findIndex(
(el) => el === '_exit_short_signal_close',
);
console.log('short_exit', colShortExitData);
@Prop({ type: Boolean, default: false }) heikinAshi!: boolean; const subplotCount =
'subplots' in props.plotConfig ? Object.keys(props.plotConfig.subplots).length + 1 : 1;
@Prop({ default: true }) readonly useUTC!: boolean; if (chartOptions.value?.dataZoom && Array.isArray(chartOptions.value?.dataZoom)) {
// Only set zoom once ...
@Prop({ required: true }) plotConfig!: PlotConfig; if (initial) {
const startingZoom = (1 - 250 / props.dataset.length) * 100;
@Prop({ default: 'dark' }) theme!: string; chartOptions.value.dataZoom.forEach((el, i) => {
if (chartOptions.value && chartOptions.value.dataZoom) {
buyData = [] as Array<number>[]; chartOptions.value.dataZoom[i].start = startingZoom;
}
sellData = [] as Array<number>[]; });
} else {
chartOptions: EChartsOption = {}; // Remove start/end settings after chart initialization to avoid chart resetting
chartOptions.value.dataZoom.forEach((el, i) => {
@Watch('dataset') if (chartOptions.value && chartOptions.value.dataZoom) {
datasetChanged() { delete chartOptions.value.dataZoom[i].start;
this.updateChart(); delete chartOptions.value.dataZoom[i].end;
}
});
}
} }
@Watch('plotConfig') const options: EChartsOption = {
plotConfigChanged() { dataset: {
this.initializeChartOptions(); source: props.heikinAshi
? heikinashi(datasetColumns.value, props.dataset.data)
: props.dataset.data,
},
grid: [
{
left: MARGINLEFT,
right: MARGINRIGHT,
// Grid Layout from bottom to top
bottom: `${subplotCount * SUBPLOTHEIGHT + 2}%`,
},
{
// Volume
left: MARGINLEFT,
right: MARGINRIGHT,
// Grid Layout from bottom to top
bottom: `${subplotCount * SUBPLOTHEIGHT}%`,
height: `${SUBPLOTHEIGHT}%`,
},
],
series: [
{
name: 'Candles',
type: 'candlestick',
barWidth: '80%',
itemStyle: {
color: upColor,
color0: downColor,
borderColor: upBorderColor,
borderColor0: downBorderColor,
},
encode: {
x: colDate,
// open, close, low, high
y: [colOpen, colClose, colLow, colHigh],
},
},
{
name: 'Volume',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#777777',
},
large: true,
encode: {
x: colDate,
y: colVolume,
},
},
{
name: 'Long',
type: 'scatter',
symbol: 'triangle',
symbolSize: 10,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: buySignalColor,
},
encode: {
x: colDate,
y: colBuyData,
},
},
{
name: 'Long exit',
type: 'scatter',
symbol: 'diamond',
symbolSize: 8,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: sellSignalColor,
},
encode: {
x: colDate,
y: colSellData,
},
},
],
};
if (hasShorts) {
// Add short support
if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
chartOptions.value.legend.data.push('Short');
chartOptions.value.legend.data.push('Short exit');
}
if (Array.isArray(options.series)) {
options.series.push({
name: 'Short',
type: 'scatter',
symbol: 'pin',
symbolSize: 10,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: shortEntrySignalColor,
},
encode: {
x: colDate,
y: colShortEntryData,
},
});
options.series.push({
name: 'Short exit',
type: 'scatter',
symbol: 'pin',
symbolSize: 8,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: shortexitSignalColor,
},
encode: {
x: colDate,
y: colShortExitData,
},
});
}
} }
@Watch('heikinAshi') // Merge this into original data
heikinAshiChanged() { Object.assign(chartOptions.value, options);
this.updateChart();
if ('main_plot' in props.plotConfig) {
Object.entries(props.plotConfig.main_plot).forEach(([key, value]) => {
const col = props.dataset.columns.findIndex((el) => el === key);
if (col > 1) {
if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
chartOptions.value.legend.data.push(key);
}
const sp: SeriesOption = {
name: key,
type: value.type || 'line',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: value.color,
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (Array.isArray(chartOptions.value?.series)) {
chartOptions.value?.series.push(sp);
}
} else {
console.log(`element ${key} for main plot not found in columns.`);
}
});
} }
get strategy() { // START Subplots
return this.dataset ? this.dataset.strategy : ''; if ('subplots' in props.plotConfig) {
let plotIndex = 2;
Object.entries(props.plotConfig.subplots).forEach(([key, value]) => {
// define yaxis
// Subplots are added from bottom to top - only the "bottom-most" plot stays at the bottom.
// const currGridIdx = totalSubplots - plotIndex > 1 ? totalSubplots - plotIndex : plotIndex;
const currGridIdx = plotIndex;
if (
Array.isArray(chartOptions.value.yAxis) &&
chartOptions.value.yAxis.length <= plotIndex
) {
chartOptions.value.yAxis.push({
scale: true,
gridIndex: currGridIdx,
name: key,
nameLocation: 'middle',
nameGap: NAMEGAP,
axisLabel: { show: true },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
});
}
if (
Array.isArray(chartOptions.value.xAxis) &&
chartOptions.value.xAxis.length <= plotIndex
) {
chartOptions.value.xAxis.push({
type: 'time',
scale: true,
gridIndex: currGridIdx,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
axisLabel: { show: false },
axisPointer: {
label: { show: false },
},
splitLine: { show: false },
splitNumber: 20,
});
}
if (Array.isArray(chartOptions.value.dataZoom)) {
chartOptions.value.dataZoom.forEach((el) =>
el.xAxisIndex && Array.isArray(el.xAxisIndex) ? el.xAxisIndex.push(plotIndex) : null,
);
}
if (chartOptions.value.grid && Array.isArray(chartOptions.value.grid)) {
chartOptions.value.grid.push({
left: MARGINLEFT,
right: MARGINRIGHT,
bottom: `${(subplotCount - plotIndex + 1) * SUBPLOTHEIGHT}%`,
height: `${SUBPLOTHEIGHT}%`,
});
}
Object.entries(value).forEach(([sk, sv]) => {
// entries per subplot
const col = props.dataset.columns.findIndex((el) => el === sk);
if (col > 0) {
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
chartOptions.value.legend.data.push(sk);
}
const sp: SeriesOption = {
name: sk,
type: sv.type || 'line',
xAxisIndex: plotIndex,
yAxisIndex: plotIndex,
itemStyle: {
color: sv.color || randomColor(),
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (chartOptions.value.series && Array.isArray(chartOptions.value.series)) {
chartOptions.value.series.push(sp);
}
} else {
console.log(`element ${sk} was not found in the columns.`);
}
});
plotIndex += 1;
});
}
// END Subplots
// Last subplot should show xAxis labels
// if (options.xAxis && Array.isArray(options.xAxis)) {
// options.xAxis[options.xAxis.length - 1].axisLabel.show = true;
// options.xAxis[options.xAxis.length - 1].axisTick.show = true;
// }
if (chartOptions.value.grid && Array.isArray(chartOptions.value.grid)) {
// Last subplot is bottom
chartOptions.value.grid[chartOptions.value.grid.length - 1].bottom = '50px';
delete chartOptions.value.grid[chartOptions.value.grid.length - 1].top;
}
const { trades, tradesClose } = getTradeEntries();
const name = 'Trades';
const nameClose = 'Trades Close';
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
chartOptions.value.legend.data.push(name);
}
const sp: ScatterSeriesOption = {
name,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: tradeBuyColor,
},
data: trades,
};
if (Array.isArray(chartOptions.value?.series)) {
chartOptions.value.series.push(sp);
}
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
chartOptions.value.legend.data.push(nameClose);
}
const closeSeries: ScatterSeriesOption = {
name: nameClose,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: tradeSellColor,
},
data: tradesClose,
};
if (chartOptions.value.series && Array.isArray(chartOptions.value.series)) {
chartOptions.value.series.push(closeSeries);
} }
get pair() { console.log('chartOptions', chartOptions.value);
return this.dataset ? this.dataset.pair : '';
}
get timeframe() { candleChart.value.setOption(chartOptions.value);
return this.dataset ? this.dataset.timeframe : ''; };
}
get timeframems() { const initializeChartOptions = () => {
return this.dataset ? this.dataset.timeframe_ms : 0; chartOptions.value = {
}
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);
}
mounted() {
this.initializeChartOptions();
}
get chartTitle() {
return `${this.strategy} - ${this.pair} - ${this.timeframe}`;
}
initializeChartOptions() {
this.chartOptions = {
title: [ title: [
{ {
// text: this.chartTitle, // text: this.chartTitle,
@ -157,7 +523,7 @@ export default class CandleChart extends Vue {
}, },
], ],
backgroundColor: 'rgba(0, 0, 0, 0)', backgroundColor: 'rgba(0, 0, 0, 0)',
useUTC: this.useUTC, useUTC: props.useUTC,
animation: false, animation: false,
legend: { legend: {
// Initial legend, further entries are pushed to the below list // Initial legend, further entries are pushed to the below list
@ -284,396 +650,9 @@ export default class CandleChart extends Vue {
}; };
console.log('Initialized'); console.log('Initialized');
this.updateChart(true); updateChart(true);
}
updateChart(initial = false) {
if (!this.hasData) {
return;
}
if (this.chartOptions?.title) {
this.chartOptions.title[0].text = this.chartTitle;
}
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');
// TODO: Remove below *signal_open after December 2021
const colBuyData = this.dataset.columns.findIndex(
(el) =>
el === '_buy_signal_open' ||
el === '_buy_signal_close' ||
el === '_enter_long_signal_close',
);
const colSellData = this.dataset.columns.findIndex(
(el) =>
el === '_sell_signal_open' ||
el === '_sell_signal_close' ||
el === '_exit_long_signal_close',
);
const hasShorts =
this.dataset.enter_short_signals &&
this.dataset.enter_short_signals > 0 &&
this.dataset.exit_short_signals &&
this.dataset.exit_short_signals > 0;
const colShortEntryData = this.dataset.columns.findIndex(
(el) => el === '_enter_short_signal_close',
);
const colShortExitData = this.dataset.columns.findIndex(
(el) => el === '_exit_short_signal_close',
);
console.log('short_exit', colShortExitData);
const subplotCount =
'subplots' in this.plotConfig ? Object.keys(this.plotConfig.subplots).length + 1 : 1;
if (this.chartOptions?.dataZoom && Array.isArray(this.chartOptions?.dataZoom)) {
// Only set zoom once ...
if (initial) {
const startingZoom = (1 - 250 / this.dataset.length) * 100;
this.chartOptions.dataZoom.forEach((el, i) => {
if (this.chartOptions && this.chartOptions.dataZoom) {
this.chartOptions.dataZoom[i].start = startingZoom;
}
});
} else {
// Remove start/end settings after chart initialization to avoid chart resetting
this.chartOptions.dataZoom.forEach((el, i) => {
if (this.chartOptions && this.chartOptions.dataZoom) {
delete this.chartOptions.dataZoom[i].start;
delete this.chartOptions.dataZoom[i].end;
}
});
}
}
const options: EChartsOption = {
dataset: {
source: this.heikinAshi
? heikinashi(this.datasetColumns, this.dataset.data)
: this.dataset.data,
},
grid: [
{
left: MARGINLEFT,
right: MARGINRIGHT,
// Grid Layout from bottom to top
bottom: `${subplotCount * SUBPLOTHEIGHT + 2}%`,
},
{
// Volume
left: MARGINLEFT,
right: MARGINRIGHT,
// Grid Layout from bottom to top
bottom: `${subplotCount * SUBPLOTHEIGHT}%`,
height: `${SUBPLOTHEIGHT}%`,
},
],
series: [
{
name: 'Candles',
type: 'candlestick',
barWidth: '80%',
itemStyle: {
color: upColor,
color0: downColor,
borderColor: upBorderColor,
borderColor0: downBorderColor,
},
encode: {
x: colDate,
// open, close, low, high
y: [colOpen, colClose, colLow, colHigh],
},
},
{
name: 'Volume',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#777777',
},
large: true,
encode: {
x: colDate,
y: colVolume,
},
},
{
name: 'Long',
type: 'scatter',
symbol: 'triangle',
symbolSize: 10,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: buySignalColor,
},
encode: {
x: colDate,
y: colBuyData,
},
},
{
name: 'Long exit',
type: 'scatter',
symbol: 'diamond',
symbolSize: 8,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: sellSignalColor,
},
encode: {
x: colDate,
y: colSellData,
},
},
],
}; };
if (hasShorts) {
// Add short support
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push('Short');
this.chartOptions.legend.data.push('Short exit');
}
if (Array.isArray(options.series)) {
options.series.push({
name: 'Short',
type: 'scatter',
symbol: 'pin',
symbolSize: 10,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: shortEntrySignalColor,
},
encode: {
x: colDate,
y: colShortEntryData,
},
});
options.series.push({
name: 'Short exit',
type: 'scatter',
symbol: 'pin',
symbolSize: 8,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: shortexitSignalColor,
},
encode: {
x: colDate,
y: colShortExitData,
},
});
}
}
// Merge this into original data
Object.assign(this.chartOptions, options);
if ('main_plot' in this.plotConfig) {
Object.entries(this.plotConfig.main_plot).forEach(([key, value]) => {
const col = this.dataset.columns.findIndex((el) => el === key);
if (col > 1) {
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push(key);
}
const sp: SeriesOption = {
name: key,
type: value.type || 'line',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: value.color,
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (Array.isArray(this.chartOptions.series)) {
this.chartOptions.series.push(sp);
}
} else {
console.log(`element ${key} for main plot not found in columns.`);
}
});
}
// START Subplots
if ('subplots' in this.plotConfig) {
let plotIndex = 2;
Object.entries(this.plotConfig.subplots).forEach(([key, value]) => {
// define yaxis
// Subplots are added from bottom to top - only the "bottom-most" plot stays at the bottom.
// const currGridIdx = totalSubplots - plotIndex > 1 ? totalSubplots - plotIndex : plotIndex;
const currGridIdx = plotIndex;
if (Array.isArray(this.chartOptions.yAxis) && this.chartOptions.yAxis.length <= plotIndex) {
this.chartOptions.yAxis.push({
scale: true,
gridIndex: currGridIdx,
name: key,
nameLocation: 'middle',
nameGap: NAMEGAP,
axisLabel: { show: true },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false },
});
}
if (Array.isArray(this.chartOptions.xAxis) && this.chartOptions.xAxis.length <= plotIndex) {
this.chartOptions.xAxis.push({
type: 'time',
scale: true,
gridIndex: currGridIdx,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
axisLabel: { show: false },
axisPointer: {
label: { show: false },
},
splitLine: { show: false },
splitNumber: 20,
});
}
if (Array.isArray(this.chartOptions.dataZoom)) {
this.chartOptions.dataZoom.forEach((el) =>
el.xAxisIndex && Array.isArray(el.xAxisIndex) ? el.xAxisIndex.push(plotIndex) : null,
);
}
if (this.chartOptions.grid && Array.isArray(this.chartOptions.grid)) {
this.chartOptions.grid.push({
left: MARGINLEFT,
right: MARGINRIGHT,
bottom: `${(subplotCount - plotIndex + 1) * SUBPLOTHEIGHT}%`,
height: `${SUBPLOTHEIGHT}%`,
});
}
Object.entries(value).forEach(([sk, sv]) => {
// entries per subplot
const col = this.dataset.columns.findIndex((el) => el === sk);
if (col > 0) {
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push(sk);
}
const sp: SeriesOption = {
name: sk,
type: sv.type || 'line',
xAxisIndex: plotIndex,
yAxisIndex: plotIndex,
itemStyle: {
color: sv.color || randomColor(),
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
if (this.chartOptions.series && Array.isArray(this.chartOptions.series)) {
this.chartOptions.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 (this.chartOptions.grid && Array.isArray(this.chartOptions.grid)) {
// Last subplot is bottom
this.chartOptions.grid[this.chartOptions.grid.length - 1].bottom = '50px';
delete this.chartOptions.grid[this.chartOptions.grid.length - 1].top;
}
const { trades, tradesClose } = this.getTradeEntries();
const name = 'Trades';
const nameClose = 'Trades Close';
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push(name);
}
const sp: ScatterSeriesOption = {
name,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: tradeBuyColor,
},
data: trades,
};
if (Array.isArray(this.chartOptions?.series)) {
this.chartOptions.series.push(sp);
}
if (!Array.isArray(this.chartOptions.legend) && this.chartOptions.legend?.data) {
this.chartOptions.legend.data.push(nameClose);
}
const closeSeries: ScatterSeriesOption = {
name: nameClose,
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: tradeSellColor,
},
data: tradesClose,
};
if (this.chartOptions.series && Array.isArray(this.chartOptions.series)) {
this.chartOptions.series.push(closeSeries);
}
console.log('chartOptions', this.chartOptions);
this.$refs.candleChart.setOption(this.chartOptions);
}
/** Return trade entries for charting */
getTradeEntries() {
const trades: (string | number)[][] = [];
const tradesClose: (string | number)[][] = [];
for (let i = 0, len = this.filteredTrades.length; i < len; i += 1) {
const trade: Trade = this.filteredTrades[i];
if (
trade.open_timestamp >= this.dataset.data_start_ts &&
trade.open_timestamp <= this.dataset.data_stop_ts
) {
trades.push([roundTimeframe(this.timeframems, trade.open_timestamp), trade.open_rate]);
}
if (
trade.close_timestamp !== undefined &&
trade.close_timestamp < this.dataset.data_stop_ts &&
trade.close_timestamp > this.dataset.data_start_ts
) {
if (trade.close_date !== undefined && trade.close_rate !== undefined) {
tradesClose.push([
roundTimeframe(this.timeframems, trade.close_timestamp),
trade.close_rate,
]);
}
}
}
return { trades, tradesClose };
}
// createSignalData(colDate: number, colOpen: number, colBuy: number, colSell: number): void { // createSignalData(colDate: number, colOpen: number, colBuy: number, colSell: number): void {
// Calculate Buy and sell Series // Calculate Buy and sell Series
// if (!this.signalsCalculated) { // if (!this.signalsCalculated) {
@ -689,12 +668,46 @@ export default class CandleChart extends Vue {
// this.signalsCalculated = true; // this.signalsCalculated = true;
// } // }
// } // }
onMounted(() => {
initializeChartOptions();
});
@Watch('useUTC') watch(
useUTCChanged() { () => props.useUTC,
this.initializeChartOptions(); () => initializeChartOptions(),
} );
}
watch(
() => props.dataset,
() => updateChart(),
);
watch(
() => props.plotConfig,
() => initializeChartOptions(),
);
watch(
() => props.heikinAshi,
() => updateChart(),
);
return {
candleChart,
buyData,
sellData,
chartOptions,
strategy,
pair,
timeframe,
timeframems,
datasetColumns,
hasData,
filteredTrades,
chartTitle,
};
},
});
</script> </script>
<style scoped> <style scoped>

View File

@ -103,7 +103,7 @@ export default defineComponent({
name: 'CandleChartContainer', name: 'CandleChartContainer',
components: { CandleChart, PlotConfigurator, vSelect }, components: { CandleChart, PlotConfigurator, vSelect },
props: { props: {
trades: { required: false, default: [], type: Array as () => Trade[] }, trades: { required: false, default: () => [], type: Array as () => Trade[] },
availablePairs: { required: true, type: Array as () => string[] }, availablePairs: { required: true, type: Array as () => string[] },
timeframe: { required: true, type: String }, timeframe: { required: true, type: String },
historicView: { required: false, default: false, type: Boolean }, historicView: { required: false, default: false, type: Boolean },

View File

@ -18,7 +18,7 @@ import {
} from 'echarts/components'; } from 'echarts/components';
import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types'; import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types';
import { defineComponent, ref, computed } from '@vue/composition-api'; import { defineComponent, ref, computed, watch } from '@vue/composition-api';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
use([ use([
@ -50,7 +50,11 @@ export default defineComponent({
setup(props) { setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const botList = ref<string[]>([]); const botList = ref<string[]>([]);
const cumulativeData = computed(() => { const cumulativeData = ref<{ date: number; profit: any }[]>([]);
watch(
() => props.trades,
() => {
botList.value = []; botList.value = [];
const res: CumProfitData[] = []; const res: CumProfitData[] = [];
const resD: CumProfitDataPerDate = {}; const resD: CumProfitDataPerDate = {};
@ -85,7 +89,7 @@ export default defineComponent({
} }
// console.log(resD); // console.log(resD);
return Object.entries(resD).map(([k, v]) => { cumulativeData.value = Object.entries(resD).map(([k, v]) => {
const obj = { date: parseInt(k, 10), profit: v.profit }; const obj = { date: parseInt(k, 10), profit: v.profit };
// TODO: The below could allow "lines" per bot" // TODO: The below could allow "lines" per bot"
// this.botList.forEach((botId) => { // this.botList.forEach((botId) => {
@ -93,7 +97,8 @@ export default defineComponent({
// }); // });
return obj; return obj;
}); });
}); },
);
const chartOptions = computed((): EChartsOption => { const chartOptions = computed((): EChartsOption => {
const chartOptionsLoc: EChartsOption = { const chartOptionsLoc: EChartsOption = {

View File

@ -59,11 +59,9 @@
<script lang="ts"> <script lang="ts">
import TradeList from '@/components/ftbot/TradeList.vue'; import TradeList from '@/components/ftbot/TradeList.vue';
import { Component, Vue, Prop } from 'vue-property-decorator';
import { StrategyBacktestResult, Trade } from '@/types'; import { StrategyBacktestResult, Trade } from '@/types';
import ValuePair from '@/components/general/ValuePair.vue'; import { defineComponent, computed } from '@vue/composition-api';
import { import {
timestampms, timestampms,
formatPercent, formatPercent,
@ -71,65 +69,62 @@ import {
humanizeDurationFromSeconds, humanizeDurationFromSeconds,
} from '@/shared/formatters'; } from '@/shared/formatters';
@Component({ export default defineComponent({
name: 'LoginModal',
components: { components: {
TradeList, TradeList,
ValuePair,
}, },
}) props: {
export default class BacktestResultView extends Vue { backtestResult: { required: true, type: Object as () => StrategyBacktestResult },
@Prop({ required: true }) readonly backtestResult!: StrategyBacktestResult; },
setup(props) {
const hasBacktestResult = computed(() => {
return !!props.backtestResult;
});
get hasBacktestResult() { const formatPriceStake = (price) => {
return !!this.backtestResult; return `${formatPrice(price, props.backtestResult.stake_currency_decimals)} ${
} props.backtestResult.stake_currency
}`;
getSortedTrades(backtestResult: StrategyBacktestResult): Trade[] { };
const getSortedTrades = (backtestResult: StrategyBacktestResult): Trade[] => {
const sortedTrades = backtestResult.trades const sortedTrades = backtestResult.trades
.slice() .slice()
.sort((a, b) => a.profit_ratio - b.profit_ratio); .sort((a, b) => a.profit_ratio - b.profit_ratio);
return sortedTrades; return sortedTrades;
} };
formatPriceStake(price) { const bestPair = computed((): string => {
return `${formatPrice(price, this.backtestResult.stake_currency_decimals)} ${ const trades = getSortedTrades(props.backtestResult);
this.backtestResult.stake_currency
}`;
}
get bestPair(): string {
const trades = this.getSortedTrades(this.backtestResult);
const value = trades[trades.length - 1]; const value = trades[trades.length - 1];
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`; return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
} });
const worstPair = computed((): string => {
get worstPair(): string { const trades = getSortedTrades(props.backtestResult);
const trades = this.getSortedTrades(this.backtestResult);
const value = trades[0]; const value = trades[0];
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`; return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
} });
const backtestResultStats = computed(() => {
get backtestResultStats() {
// Transpose Result into readable format // Transpose Result into readable format
const shortMetrics = const shortMetrics =
this.backtestResult?.trade_count_short && this.backtestResult?.trade_count_short > 0 props.backtestResult?.trade_count_short && props.backtestResult?.trade_count_short > 0
? [ ? [
{ metric: '___', value: '___' }, { metric: '___', value: '___' },
{ {
metric: 'Long / Short', metric: 'Long / Short',
value: `${this.backtestResult.trade_count_long} / ${this.backtestResult.trade_count_short}`, value: `${props.backtestResult.trade_count_long} / ${props.backtestResult.trade_count_short}`,
}, },
{ {
metric: 'Total profit Long', metric: 'Total profit Long',
value: `${formatPercent( value: `${formatPercent(
this.backtestResult.profit_total_long || 0, props.backtestResult.profit_total_long || 0,
)} | ${this.formatPriceStake(this.backtestResult.profit_total_long_abs)}`, )} | ${formatPriceStake(props.backtestResult.profit_total_long_abs)}`,
}, },
{ {
metric: 'Total profit Short', metric: 'Total profit Short',
value: `${formatPercent( value: `${formatPercent(
this.backtestResult.profit_total_short || 0, props.backtestResult.profit_total_short || 0,
)} | ${this.formatPriceStake(this.backtestResult.profit_total_short_abs)}`, )} | ${formatPriceStake(props.backtestResult.profit_total_short_abs)}`,
}, },
] ]
: []; : [];
@ -137,186 +132,184 @@ export default class BacktestResultView extends Vue {
return [ return [
{ {
metric: 'Total Profit', metric: 'Total Profit',
value: `${formatPercent(this.backtestResult.profit_total)} | ${this.formatPriceStake( value: `${formatPercent(props.backtestResult.profit_total)} | ${formatPriceStake(
this.backtestResult.profit_total_abs, props.backtestResult.profit_total_abs,
)}`, )}`,
}, },
{ {
metric: 'Total trades / Daily Avg Trades', metric: 'Total trades / Daily Avg Trades',
value: `${this.backtestResult.total_trades} / ${this.backtestResult.trades_per_day}`, value: `${props.backtestResult.total_trades} / ${props.backtestResult.trades_per_day}`,
}, },
// { metric: 'First trade', value: this.backtestResult.backtest_fi }, // { metric: 'First trade', value: props.backtestResult.backtest_fi },
// { metric: 'First trade Pair', value: this.backtestResult.backtest_best_day }, // { metric: 'First trade Pair', value: props.backtestResult.backtest_best_day },
{ {
metric: 'Best day', metric: 'Best day',
value: `${formatPercent( value: `${formatPercent(props.backtestResult.backtest_best_day, 2)} | ${formatPriceStake(
this.backtestResult.backtest_best_day, props.backtestResult.backtest_best_day_abs,
2, )}`,
)} | ${this.formatPriceStake(this.backtestResult.backtest_best_day_abs)}`,
}, },
{ {
metric: 'Worst day', metric: 'Worst day',
value: `${formatPercent( value: `${formatPercent(props.backtestResult.backtest_worst_day, 2)} | ${formatPriceStake(
this.backtestResult.backtest_worst_day, props.backtestResult.backtest_worst_day_abs,
2, )}`,
)} | ${this.formatPriceStake(this.backtestResult.backtest_worst_day_abs)}`,
}, },
{ {
metric: 'Win/Draw/Loss', metric: 'Win/Draw/Loss',
value: `${ value: `${
this.backtestResult.results_per_pair[this.backtestResult.results_per_pair.length - 1].wins props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
.wins
} / ${ } / ${
this.backtestResult.results_per_pair[this.backtestResult.results_per_pair.length - 1] props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
.draws .draws
} / ${ } / ${
this.backtestResult.results_per_pair[this.backtestResult.results_per_pair.length - 1] props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
.losses .losses
}`, }`,
}, },
{ {
metric: 'Days win/draw/loss', metric: 'Days win/draw/loss',
value: `${this.backtestResult.winning_days} / ${this.backtestResult.draw_days} / ${this.backtestResult.losing_days}`, value: `${props.backtestResult.winning_days} / ${props.backtestResult.draw_days} / ${props.backtestResult.losing_days}`,
}, },
{ {
metric: 'Avg. Duration winners', metric: 'Avg. Duration winners',
value: humanizeDurationFromSeconds(this.backtestResult.winner_holding_avg), value: humanizeDurationFromSeconds(props.backtestResult.winner_holding_avg),
}, },
{ {
metric: 'Avg. Duration Losers', metric: 'Avg. Duration Losers',
value: humanizeDurationFromSeconds(this.backtestResult.loser_holding_avg), value: humanizeDurationFromSeconds(props.backtestResult.loser_holding_avg),
}, },
{ metric: 'Rejected entry signals', value: this.backtestResult.rejected_signals }, { metric: 'Rejected entry signals', value: props.backtestResult.rejected_signals },
{ {
metric: 'Entry/Exit timeouts', metric: 'Entry/Exit timeouts',
value: `${this.backtestResult.timedout_entry_orders} / ${this.backtestResult.timedout_exit_orders}`, value: `${props.backtestResult.timedout_entry_orders} / ${props.backtestResult.timedout_exit_orders}`,
}, },
...shortMetrics, ...shortMetrics,
{ metric: '___', value: '___' }, { metric: '___', value: '___' },
{ metric: 'Min balance', value: this.formatPriceStake(this.backtestResult.csum_min) }, { metric: 'Min balance', value: formatPriceStake(props.backtestResult.csum_min) },
{ metric: 'Max balance', value: this.formatPriceStake(this.backtestResult.csum_max) }, { metric: 'Max balance', value: formatPriceStake(props.backtestResult.csum_max) },
{ metric: 'Market change', value: formatPercent(this.backtestResult.market_change) }, { metric: 'Market change', value: formatPercent(props.backtestResult.market_change) },
{ metric: '___', value: '___' }, { metric: '___', value: '___' },
{ {
metric: 'Max Drawdown (Account)', metric: 'Max Drawdown (Account)',
value: formatPercent(this.backtestResult.max_drawdown_account), value: formatPercent(props.backtestResult.max_drawdown_account),
}, },
{ {
metric: 'Max Drawdown ABS', metric: 'Max Drawdown ABS',
value: this.formatPriceStake(this.backtestResult.max_drawdown_abs), value: formatPriceStake(props.backtestResult.max_drawdown_abs),
}, },
{ {
metric: 'Drawdown high | low', metric: 'Drawdown high | low',
value: `${this.formatPriceStake( value: `${formatPriceStake(props.backtestResult.max_drawdown_high)} | ${formatPriceStake(
this.backtestResult.max_drawdown_high, props.backtestResult.max_drawdown_low,
)} | ${this.formatPriceStake(this.backtestResult.max_drawdown_low)}`, )}`,
}, },
{ metric: 'Drawdown start', value: timestampms(this.backtestResult.drawdown_start_ts) }, { metric: 'Drawdown start', value: timestampms(props.backtestResult.drawdown_start_ts) },
{ metric: 'Drawdown end', value: timestampms(this.backtestResult.drawdown_end_ts) }, { metric: 'Drawdown end', value: timestampms(props.backtestResult.drawdown_end_ts) },
{ metric: '___', value: '___' }, { metric: '___', value: '___' },
{ {
metric: 'Best Pair', metric: 'Best Pair',
value: `${this.backtestResult.best_pair.key} ${formatPercent( value: `${props.backtestResult.best_pair.key} ${formatPercent(
this.backtestResult.best_pair.profit_sum, props.backtestResult.best_pair.profit_sum,
)}`, )}`,
}, },
{ {
metric: 'Worst Pair', metric: 'Worst Pair',
value: `${this.backtestResult.worst_pair.key} ${formatPercent( value: `${props.backtestResult.worst_pair.key} ${formatPercent(
this.backtestResult.worst_pair.profit_sum, props.backtestResult.worst_pair.profit_sum,
)}`, )}`,
}, },
{ metric: 'Best single Trade', value: this.bestPair }, { metric: 'Best single Trade', value: bestPair },
{ metric: 'Worst single Trade', value: this.worstPair }, { metric: 'Worst single Trade', value: worstPair },
]; ];
} });
timestampms = timestampms; const backtestResultSettings = computed(() => {
formatPercent = formatPercent;
get backtestResultSettings() {
// Transpose Result into readable format // Transpose Result into readable format
return [ return [
{ setting: 'Backtesting from', value: timestampms(this.backtestResult.backtest_start_ts) }, { setting: 'Backtesting from', value: timestampms(props.backtestResult.backtest_start_ts) },
{ setting: 'Backtesting to', value: timestampms(this.backtestResult.backtest_end_ts) }, { setting: 'Backtesting to', value: timestampms(props.backtestResult.backtest_end_ts) },
{ {
setting: 'BT execution time', setting: 'BT execution time',
value: humanizeDurationFromSeconds( value: humanizeDurationFromSeconds(
this.backtestResult.backtest_run_end_ts - this.backtestResult.backtest_run_start_ts, props.backtestResult.backtest_run_end_ts - props.backtestResult.backtest_run_start_ts,
), ),
}, },
{ setting: 'Max open trades', value: this.backtestResult.max_open_trades }, { setting: 'Max open trades', value: props.backtestResult.max_open_trades },
{ setting: 'Timeframe', value: this.backtestResult.timeframe }, { setting: 'Timeframe', value: props.backtestResult.timeframe },
{ setting: 'Timerange', value: this.backtestResult.timerange }, { setting: 'Timerange', value: props.backtestResult.timerange },
{ setting: 'Stoploss', value: formatPercent(this.backtestResult.stoploss, 2) }, { setting: 'Stoploss', value: formatPercent(props.backtestResult.stoploss, 2) },
{ setting: 'Trailing Stoploss', value: this.backtestResult.trailing_stop }, { setting: 'Trailing Stoploss', value: props.backtestResult.trailing_stop },
{ {
setting: 'Trail only when offset is reached', setting: 'Trail only when offset is reached',
value: this.backtestResult.trailing_only_offset_is_reached, value: props.backtestResult.trailing_only_offset_is_reached,
}, },
{ setting: 'Trailing Stop positive', value: this.backtestResult.trailing_stop_positive }, { setting: 'Trailing Stop positive', value: props.backtestResult.trailing_stop_positive },
{ {
setting: 'Trailing stop positive offset', setting: 'Trailing stop positive offset',
value: this.backtestResult.trailing_stop_positive_offset, value: props.backtestResult.trailing_stop_positive_offset,
}, },
{ setting: 'Custom Stoploss', value: this.backtestResult.use_custom_stoploss }, { setting: 'Custom Stoploss', value: props.backtestResult.use_custom_stoploss },
{ setting: 'ROI', value: this.backtestResult.minimal_roi }, { setting: 'ROI', value: props.backtestResult.minimal_roi },
{ {
setting: 'Use Exit Signal', setting: 'Use Exit Signal',
value: value:
this.backtestResult.use_exit_signal !== undefined props.backtestResult.use_exit_signal !== undefined
? this.backtestResult.use_exit_signal ? props.backtestResult.use_exit_signal
: this.backtestResult.use_sell_signal, : props.backtestResult.use_sell_signal,
}, },
{ {
setting: 'Exit profit only', setting: 'Exit profit only',
value: value:
this.backtestResult.exit_profit_only !== undefined props.backtestResult.exit_profit_only !== undefined
? this.backtestResult.exit_profit_only ? props.backtestResult.exit_profit_only
: this.backtestResult.sell_profit_only, : props.backtestResult.sell_profit_only,
}, },
{ {
setting: 'Exit profit offset', setting: 'Exit profit offset',
value: value:
this.backtestResult.exit_profit_offset !== undefined props.backtestResult.exit_profit_offset !== undefined
? this.backtestResult.exit_profit_offset ? props.backtestResult.exit_profit_offset
: this.backtestResult.sell_profit_offset, : props.backtestResult.sell_profit_offset,
}, },
{ setting: 'Enable protections', value: this.backtestResult.enable_protections }, { setting: 'Enable protections', value: props.backtestResult.enable_protections },
{ {
setting: 'Starting balance', setting: 'Starting balance',
value: this.formatPriceStake(this.backtestResult.starting_balance), value: formatPriceStake(props.backtestResult.starting_balance),
}, },
{ {
setting: 'Final balance', setting: 'Final balance',
value: this.formatPriceStake(this.backtestResult.final_balance), value: formatPriceStake(props.backtestResult.final_balance),
}, },
{ {
setting: 'Avg. stake amount', setting: 'Avg. stake amount',
value: this.formatPriceStake(this.backtestResult.avg_stake_amount), value: formatPriceStake(props.backtestResult.avg_stake_amount),
}, },
{ {
setting: 'Total trade volume', setting: 'Total trade volume',
value: this.formatPriceStake(this.backtestResult.total_volume), value: formatPriceStake(props.backtestResult.total_volume),
}, },
]; ];
} });
const perPairFields = computed(() => {
get perPairFields() {
return [ return [
{ key: 'key', label: 'Pair' }, { key: 'key', label: 'Pair' },
{ key: 'trades', label: 'Buys' }, { key: 'trades', label: 'Buys' },
{ key: 'profit_mean', label: 'Avg Profit %', formatter: (value) => formatPercent(value, 2) }, {
key: 'profit_mean',
label: 'Avg Profit %',
formatter: (value) => formatPercent(value, 2),
},
{ key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) }, { key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) },
{ {
key: 'profit_total_abs', key: 'profit_total_abs',
label: `Tot Profit ${this.backtestResult.stake_currency}`, label: `Tot Profit ${props.backtestResult.stake_currency}`,
formatter: (value) => formatPrice(value, this.backtestResult.stake_currency_decimals), formatter: (value) => formatPrice(value, props.backtestResult.stake_currency_decimals),
}, },
{ {
key: 'profit_total', key: 'profit_total',
@ -328,19 +321,23 @@ export default class BacktestResultView extends Vue {
{ key: 'draws', label: 'Draws' }, { key: 'draws', label: 'Draws' },
{ key: 'losses', label: 'Losses' }, { key: 'losses', label: 'Losses' },
]; ];
} });
get perExitReason() { const perExitReason = computed(() => {
return [ return [
{ key: 'exit_reason', label: 'Exit Reason' }, { key: 'exit_reason', label: 'Exit Reason' },
{ key: 'trades', label: 'Buys' }, { key: 'trades', label: 'Buys' },
{ key: 'profit_mean', label: 'Avg Profit %', formatter: (value) => formatPercent(value, 2) }, {
key: 'profit_mean',
label: 'Avg Profit %',
formatter: (value) => formatPercent(value, 2),
},
{ key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) }, { key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) },
{ {
key: 'profit_total_abs', key: 'profit_total_abs',
label: `Tot Profit ${this.backtestResult.stake_currency}`, label: `Tot Profit ${props.backtestResult.stake_currency}`,
formatter: (value) => formatPrice(value, this.backtestResult.stake_currency_decimals), formatter: (value) => formatPrice(value, props.backtestResult.stake_currency_decimals),
}, },
{ {
key: 'profit_total', key: 'profit_total',
@ -351,18 +348,31 @@ export default class BacktestResultView extends Vue {
{ key: 'draws', label: 'Draws' }, { key: 'draws', label: 'Draws' },
{ key: 'losses', label: 'Losses' }, { key: 'losses', label: 'Losses' },
]; ];
} });
const backtestResultFields: Array<Record<string, string>> = [
backtestResultFields: Array<Record<string, string>> = [
{ key: 'metric', label: 'Metric' }, { key: 'metric', label: 'Metric' },
{ key: 'value', label: 'Value' }, { key: 'value', label: 'Value' },
]; ];
backtestsettingFields: Array<Record<string, string>> = [ const backtestsettingFields: Array<Record<string, string>> = [
{ key: 'setting', label: 'Setting' }, { key: 'setting', label: 'Setting' },
{ key: 'value', label: 'Value' }, { key: 'value', label: 'Value' },
]; ];
}
return {
hasBacktestResult,
formatPriceStake,
bestPair,
worstPair,
backtestResultStats,
backtestResultSettings,
perPairFields,
perExitReason,
backtestResultFields,
backtestsettingFields,
};
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -37,61 +37,64 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Emit, Prop, Watch } from 'vue-property-decorator';
import { dateFromString, dateStringToTimeRange, timestampToDateString } from '@/shared/formatters'; import { dateFromString, dateStringToTimeRange, timestampToDateString } from '@/shared/formatters';
import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
const now = new Date(); const now = new Date();
@Component({})
export default class TimeRangeSelect extends Vue {
dateFrom = '';
dateTo = ''; export default defineComponent({
name: 'TimeRangeSelect',
props: {
value: { required: true, type: String },
},
setup(props, { emit }) {
const dateFrom = ref<string>('');
const dateTo = ref<string>('');
@Prop() value!: string; const timeRange = computed(() => {
if (dateFrom.value !== '' || dateTo.value !== '') {
@Emit('input') return `${dateStringToTimeRange(dateFrom.value)}-${dateStringToTimeRange(dateTo.value)}`;
emitTimeRange() {
return this.timeRange;
}
@Watch('value')
valueChanged(val) {
console.log('TimeRange', val);
if (val !== this.value) {
this.updateInput();
}
}
updateInput() {
const tr = this.value.split('-');
if (tr[0]) {
this.dateFrom = timestampToDateString(dateFromString(tr[0], 'yyyyMMdd'));
}
if (tr.length > 1 && tr[1]) {
this.dateTo = timestampToDateString(dateFromString(tr[1], 'yyyyMMdd'));
}
}
created() {
if (!this.value) {
this.dateFrom = timestampToDateString(new Date(now.getFullYear(), now.getMonth() - 1, 1));
} else {
this.updateInput();
}
this.emitTimeRange();
}
updated() {
this.emitTimeRange();
}
get timeRange() {
if (this.dateFrom !== '' || this.dateTo !== '') {
return `${dateStringToTimeRange(this.dateFrom)}-${dateStringToTimeRange(this.dateTo)}`;
} }
return ''; return '';
});
const updated = () => {
emit('input', timeRange.value);
};
const updateInput = () => {
const tr = props.value.split('-');
if (tr[0]) {
dateFrom.value = timestampToDateString(dateFromString(tr[0], 'yyyyMMdd'));
} }
if (tr.length > 1 && tr[1]) {
dateTo.value = timestampToDateString(dateFromString(tr[1], 'yyyyMMdd'));
} }
updated();
};
watch(
() => timeRange.value,
() => updated(),
);
onMounted(() => {
if (!props.value) {
dateFrom.value = timestampToDateString(new Date(now.getFullYear(), now.getMonth() - 1, 1));
} else {
updateInput();
}
emit('input', timeRange.value);
});
return {
dateFrom,
dateTo,
timeRange,
updated,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>