Skip to content

Commit

Permalink
Merge pull request #1987 from freqtrade/feat/plot_template
Browse files Browse the repository at this point in the history
Add Plot Template functionality
  • Loading branch information
xmatthias authored Jul 24, 2024
2 parents 05ce049 + c5d3d0e commit e627395
Show file tree
Hide file tree
Showing 20 changed files with 519 additions and 31 deletions.
30 changes: 30 additions & 0 deletions e2e/chart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,34 @@ test.describe('Chart', () => {
.locator('#plotConfigSelect'),
).toHaveValue('default');
});

test('Plot configurator', async ({ page }) => {
await Promise.all([
page.goto('/graph'),
page.waitForResponse('**/whitelist'),
page.waitForResponse('**/blacklist'),
]);

// Wait for the chart to load
await page.waitForSelector('span:has-text("NoActionStrategyFut | 1m")');

await page.getByRole('button', { name: 'Plot configurator' }).click();
await page.getByRole('button', { name: 'Indicator from template' }).click();
// Apply bollinger bands
await page.getByLabel('Select Templates').selectOption('BollingerBands');
// Select template - Try to use
await page.getByRole('button', { name: 'Use Template' }).click();
// Accept remapping and close
await page.getByRole('button', { name: 'Apply Template' }).click();
await page.getByRole('button', { name: 'Save' }).click();
// Close Plot configurator
await page.getByRole('button', { name: 'Plot configurator' }).click();

await expect(page.locator('canvas')).toHaveScreenshot('Chart-Plot-with_BollingerBands.png', {
threshold: 0.15,
maxDiffPixelRatio: 0.15,
});
// Should assert if indicators have been set
// but it's a canvas ...
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"core-js": "^3.37.1",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"deepmerge": "^4.3.1",
"echarts": "^5.5.1",
"favico.js": "^0.3.10",
"humanize-duration": "^3.32.1",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 13 additions & 7 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ declare global {
const ColorPreferences: typeof import('./stores/colors')['ColorPreferences']
const DashboardLayout: typeof import('./stores/layout')['DashboardLayout']
const EffectScope: typeof import('vue')['EffectScope']
const KeyCode: typeof import('./composables/inputListener')['KeyCode']
const OpenTradeVizOptions: typeof import('./stores/settings')['OpenTradeVizOptions']
const ROUND_CLOSER: typeof import('./utils/roundTimeframe')['ROUND_CLOSER']
const ROUND_DOWN: typeof import('./utils/roundTimeframe')['ROUND_DOWN']
Expand Down Expand Up @@ -44,6 +43,7 @@ declare global {
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const deepClone: typeof import('./utils/deepClone')['deepClone']
const deepMerge: typeof import('./utils/deepMerge')['deepMerge']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
Expand All @@ -66,10 +66,8 @@ declare global {
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDiffColumnsFromPlotConfig: typeof import('./utils/charts/areaPlotDataset')['getDiffColumnsFromPlotConfig']
const getTheme: typeof import('./utils/themes')['getTheme']
const getTradeEntries: typeof import('./utils/charts/tradeChartData')['getTradeEntries']
const h: typeof import('vue')['h']
const heikinAshiDataset: typeof import('./utils/charts/heikinashi')['heikinAshiDataset']
const heikinashi: typeof import('./utils/charts/heikinashi')['default']
const heikinAshiDataset: typeof import('./utils/charts/heikinAshiDataset')['heikinAshiDataset']
const humanizeDurationFromSeconds: typeof import('./utils/formatters/timeformat')['humanizeDurationFromSeconds']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const initBots: typeof import('./stores/ftbotwrapper')['initBots']
Expand Down Expand Up @@ -110,6 +108,7 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const plotConfigColumns: typeof import('./utils/charts/plotConfigColumns')['plotConfigColumns']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const randomColor: typeof import('./utils/randomColor')['default']
Expand Down Expand Up @@ -242,7 +241,6 @@ declare global {
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useInputListener: typeof import('./composables/inputListener')['useInputListener']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
Expand Down Expand Up @@ -276,6 +274,7 @@ declare global {
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePlotConfigStore: typeof import('./stores/plotConfig')['usePlotConfigStore']
const usePlotTemplates: typeof import('./composables/plotTemplates')['usePlotTemplates']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
Expand Down Expand Up @@ -339,6 +338,7 @@ declare global {
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const usedColumns: typeof import('./utils/charts/usedColumns')['default']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
Expand Down Expand Up @@ -414,6 +414,7 @@ declare module 'vue' {
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly deepClone: UnwrapRef<typeof import('./utils/deepClone')['deepClone']>
readonly deepMerge: UnwrapRef<typeof import('./utils/deepMerge')['deepMerge']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
Expand All @@ -437,7 +438,7 @@ declare module 'vue' {
readonly getDiffColumnsFromPlotConfig: UnwrapRef<typeof import('./utils/charts/areaPlotDataset')['getDiffColumnsFromPlotConfig']>
readonly getTheme: UnwrapRef<typeof import('./utils/themes')['getTheme']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly heikinAshiDataset: UnwrapRef<typeof import('./utils/charts/heikinashi')['heikinAshiDataset']>
readonly heikinAshiDataset: UnwrapRef<typeof import('./utils/charts/heikinAshiDataset')['heikinAshiDataset']>
readonly humanizeDurationFromSeconds: UnwrapRef<typeof import('./utils/formatters/timeformat')['humanizeDurationFromSeconds']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly initBots: UnwrapRef<typeof import('./stores/ftbotwrapper')['initBots']>
Expand Down Expand Up @@ -478,6 +479,7 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly plotConfigColumns: UnwrapRef<typeof import('./utils/charts/plotConfigColumns')['plotConfigColumns']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly randomColor: UnwrapRef<typeof import('./utils/randomColor')['default']>
Expand Down Expand Up @@ -643,6 +645,7 @@ declare module 'vue' {
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePlotConfigStore: UnwrapRef<typeof import('./stores/plotConfig')['usePlotConfigStore']>
readonly usePlotTemplates: UnwrapRef<typeof import('./composables/plotTemplates')['usePlotTemplates']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
Expand Down Expand Up @@ -765,6 +768,7 @@ declare module '@vue/runtime-core' {
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly deepClone: UnwrapRef<typeof import('./utils/deepClone')['deepClone']>
readonly deepMerge: UnwrapRef<typeof import('./utils/deepMerge')['deepMerge']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
Expand All @@ -788,7 +792,7 @@ declare module '@vue/runtime-core' {
readonly getDiffColumnsFromPlotConfig: UnwrapRef<typeof import('./utils/charts/areaPlotDataset')['getDiffColumnsFromPlotConfig']>
readonly getTheme: UnwrapRef<typeof import('./utils/themes')['getTheme']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly heikinAshiDataset: UnwrapRef<typeof import('./utils/charts/heikinashi')['heikinAshiDataset']>
readonly heikinAshiDataset: UnwrapRef<typeof import('./utils/charts/heikinAshiDataset')['heikinAshiDataset']>
readonly humanizeDurationFromSeconds: UnwrapRef<typeof import('./utils/formatters/timeformat')['humanizeDurationFromSeconds']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly initBots: UnwrapRef<typeof import('./stores/ftbotwrapper')['initBots']>
Expand Down Expand Up @@ -829,6 +833,7 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly plotConfigColumns: UnwrapRef<typeof import('./utils/charts/plotConfigColumns')['plotConfigColumns']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly randomColor: UnwrapRef<typeof import('./utils/randomColor')['default']>
Expand Down Expand Up @@ -994,6 +999,7 @@ declare module '@vue/runtime-core' {
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePlotConfigStore: UnwrapRef<typeof import('./stores/plotConfig')['usePlotConfigStore']>
readonly usePlotTemplates: UnwrapRef<typeof import('./composables/plotTemplates')['usePlotTemplates']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
Expand Down
2 changes: 1 addition & 1 deletion src/components/charts/CandleChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function updateChart(initial = false) {
}
}
let dataset = props.heikinAshi
? heikinashi(columns, props.dataset.data)
? heikinAshiDataset(columns, props.dataset.data)
: props.dataset.data.slice();
diffCols.value.forEach(([colFrom, colTo]) => {
Expand Down
19 changes: 13 additions & 6 deletions src/components/charts/CandleChartContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
</div>
</div>
<div class="ms-auto d-flex align-items-center w-auto">
<BFormCheckbox v-model="settingsStore.useHeikinAshiCandles"
><small class="text-nowrap">Heikin Ashi</small></BFormCheckbox
>
<BFormCheckbox v-model="settingsStore.useHeikinAshiCandles">
<small class="text-nowrap">Heikin Ashi</small>
</BFormCheckbox>

<div class="ms-2">
<PlotConfigSelect></PlotConfigSelect>
Expand Down Expand Up @@ -149,6 +149,10 @@ const strategyName = computed(() => props.strategy || dataset.value?.strategy ||
const datasetColumns = computed(() =>
dataset.value ? (dataset.value.all_columns ?? dataset.value.columns) : [],
);
const datasetLoadedColumns = computed(() =>
dataset.value ? (dataset.value.columns ?? dataset.value.all_columns) : [],
);
const hasDataset = computed(() => dataset.value && dataset.value.data.length > 0);
const isLoadingDataset = computed((): boolean => {
if (props.historicView) {
Expand Down Expand Up @@ -236,9 +240,12 @@ watch(
watch(
() => plotStore.plotConfig,
() => {
// all plotstore.usedColumns are in the dataset
const hasAllColumns = plotStore.usedColumns.every((c) => datasetColumns.value.includes(c));
if (settingsStore.useReducedPairCalls && !hasAllColumns) {
// Trigger reload if the used columns are not loaded yet but would be available.
const hasAllColumns = plotStore.usedColumns.some(
(c) => datasetColumns.value.includes(c) && !datasetLoadedColumns.value.includes(c),
);
if (settingsStore.useReducedPairCalls && hasAllColumns) {
console.log('triggering refresh');
refresh();
}
Expand Down
21 changes: 17 additions & 4 deletions src/components/charts/PlotConfigurator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@
>
Remove indicator
</BButton>
<BButton
variant="secondary"
title="Load indicator config from template"
size="sm"
@click="fromPlotTemplateVisible = !fromPlotTemplateVisible"
>
Indicator from template
</BButton>
<BButton
variant="primary"
title="Add indicator to plot"
Expand All @@ -65,7 +73,14 @@
@indicator-selected="addNewIndicatorSelected"
/>

<PlotIndicator v-if="selIndicatorName" v-model="selIndicator" class="mt-1" :columns="columns" />
<PlotFromTemplate v-model:visible="fromPlotTemplateVisible" :columns="columns" />

<PlotIndicator
v-if="selIndicatorName && !fromPlotTemplateVisible"
v-model="selIndicator"
class="mt-1"
:columns="columns"
/>
<hr />

<div class="d-flex flex-row">
Expand Down Expand Up @@ -150,9 +165,6 @@
<script setup lang="ts">
import { IndicatorConfig, PlotConfig } from '@/types';
import { useBotStore } from '@/stores/ftbotwrapper';
import { usePlotConfigStore } from '@/stores/plotConfig';
const props = defineProps({
columns: { required: true, type: Array as () => string[] },
isVisible: { required: true, default: false, type: Boolean },
Expand Down Expand Up @@ -371,6 +383,7 @@ watch(
}
},
);
const fromPlotTemplateVisible = ref(false);
</script>

<style scoped lang="scss">
Expand Down
100 changes: 100 additions & 0 deletions src/components/charts/PlotFromTemplate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script setup lang="ts">
const visible = defineModel<boolean>('visible');
defineProps<{
columns: string[];
}>();
const { plotTemplateNames, applyPlotTemplate, getTemplateContent } = usePlotTemplates();
const plotStore = usePlotConfigStore();
function fromTemplateApply() {
if (selTemplateName.value) {
plotStore.editablePlotConfig = {
...applyPlotTemplate(selTemplateName.value, plotStore.editablePlotConfig, indicatorMap.value),
};
visible.value = false;
}
}
function clickStartUseTemplate() {
showIndicatorMapping.value = !showIndicatorMapping.value;
indicatorMap.value = plotConfigColumns(getTemplateContent(selTemplateName.value)).reduce(
(acc, indicator) => {
acc[indicator] = indicator;
return acc;
},
{},
);
}
const selTemplateName = ref<string>('');
watch(
() => visible.value,
(v) => {
if (v) {
selTemplateName.value = '';
showIndicatorMapping.value = false;
}
},
);
const indicatorMap = ref<Record<string, string>>({});
const showIndicatorMapping = ref(false);
</script>

<template>
<div v-if="visible" class="pt-1">
<BFormGroup v-if="!showIndicatorMapping" label="Select Templates" label-for="selectTemplate">
<BFormSelect
id="selectTemplate"
v-model="selTemplateName"
:options="plotTemplateNames"
:select-size="4"
>
</BFormSelect>
</BFormGroup>
<div v-else>
<h5 class="mt-1 text-center">Re-map indicators</h5>
<div v-for="indicator in Object.keys(indicatorMap)" :key="indicator">
<div class="d-flex gap-2 align-items-center">
<span class="flex-grow-1 w-100">{{ indicator }}</span>
<BFormSelect
:id="`indicator-${indicator}`"
v-model="indicatorMap[indicator]"
class="flex-grow-1 w-100"
:options="columns"
>
</BFormSelect>
</div>
</div>
</div>
<div class="mt-2 d-flex gap-1 justify-content-end">
<BButton size="sm" title="Abort" variant="secondary" @click="visible = false">
<i-mdi-close />
</BButton>
<BButton
v-if="!showIndicatorMapping"
:disabled="!selTemplateName"
size="sm"
style="width: 33%"
title="Use template"
variant="primary"
@click="clickStartUseTemplate"
>
<i-mdi-check class="me-1" />Use Template
</BButton>
<BButton
v-if="showIndicatorMapping"
:disabled="!selTemplateName"
size="sm"
style="width: 33%"
title="Apply template"
variant="primary"
@click="fromTemplateApply"
>
<i-mdi-check class="me-1" />Apply Template
</BButton>
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion src/components/charts/PlotIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const props = defineProps({
const emit = defineEmits<{ 'update:modelValue': [value: IndicatorConfig] }>();
const selColor = ref(randomColor());
const graphType = ref<ChartType>(ChartType.line);
const graphType = ref<ChartType | keyof typeof ChartType>(ChartType.line);
const availableGraphTypes = ref(Object.keys(ChartType));
const selAvailableIndicator = ref('');
const cancelled = ref(false);
Expand Down
Loading

0 comments on commit e627395

Please sign in to comment.