mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-14 12:13:52 +00:00
composition: migrate remaining components
This commit is contained in:
parent
2baabf7f04
commit
e969cf83a8
|
@ -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
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user