Merge branch 'main' into feat/pairlistconfig

This commit is contained in:
Tako 2023-05-23 18:34:57 +00:00
commit 36737ae6dc
90 changed files with 15527 additions and 3068 deletions

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:18-bullseye FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bullseye
RUN sudo apt-get update \ RUN sudo apt-get update \
&& sudo apt-get install -y vim \ && sudo apt-get install -y vim \

View File

@ -11,6 +11,8 @@
"source=frequi-bashhistory,target=/home/node/commandhistory,type=volume" "source=frequi-bashhistory,target=/home/node/commandhistory,type=volume"
], ],
"remoteUser": "node", "remoteUser": "node",
"customizations": {
"vscode": {
"settings": { "settings": {
// "editor.formatOnSave": true, // "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
@ -20,7 +22,7 @@
"vue": "html", "vue": "html",
"vue-html": "html" "vue-html": "html"
}, },
"workbench.iconTheme": "vscode-icons", "workbench.iconTheme": "vscode-icons"
}, },
"extensions": [ "extensions": [
"vue.volar", "vue.volar",
@ -30,6 +32,10 @@
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"hediet.vscode-drawio", "hediet.vscode-drawio",
], "ZixuanChen.vitest-explorer",
"antfu.iconify"
]
}
},
"postCreateCommand": "yarn install", "postCreateCommand": "yarn install",
} }

View File

@ -21,7 +21,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ ubuntu-22.04 ] os: [ ubuntu-22.04 ]
node: [ "16", "18", "19"] node: [ "16", "18", "19", "20"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ yarn-error.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
components.d.ts

View File

@ -1,4 +1,4 @@
FROM node:19.9.0-alpine as ui-builder FROM node:20.2.0-alpine as ui-builder
RUN mkdir /app RUN mkdir /app

View File

@ -24,4 +24,8 @@ export function defaultMocks() {
cy.intercept('GET', '**/api/v1/show_config', { cy.intercept('GET', '**/api/v1/show_config', {
fixture: 'show_config.json', fixture: 'show_config.json',
}).as('ShowConf'); }).as('ShowConf');
cy.intercept('GET', '**/api/v1/pair_candles?*', {
fixture: 'pair_candles_btc_1m.json',
}).as('PairCandles');
} }

File diff suppressed because it is too large Load Diff

View File

@ -17,53 +17,55 @@
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@vuepic/vue-datepicker": "^4.4.0", "@vuepic/vue-datepicker": "^5.1.1",
"@vueuse/core": "^10.0.2", "@vueuse/core": "^10.1.2",
"@vueuse/integrations": "^10.0.2", "@vueuse/integrations": "^10.1.2",
"axios": "^1.3.5", "axios": "^1.4.0",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"bootstrap-vue-next": "^0.8.5", "bootstrap-vue-next": "^0.8.11",
"core-js": "^3.30.1", "core-js": "^3.30.2",
"date-fns": "^2.29.3", "date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0", "date-fns-tz": "^2.0.0",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"favico.js": "^0.3.10", "favico.js": "^0.3.10",
"humanize-duration": "^3.28.0", "humanize-duration": "^3.28.0",
"pinia": "^2.0.34", "pinia": "^2.0.36",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"vue": "^3.2.47", "vue": "^3.3.2",
"vue-class-component": "^7.2.5", "vue-class-component": "^7.2.5",
"vue-demi": "^0.14.0", "vue-demi": "^0.14.1",
"vue-echarts": "^6.5.4", "vue-echarts": "^6.5.5",
"vue-material-design-icons": "^5.2.0", "vue-router": "^4.2.0",
"vue-router": "^4.1.6",
"vue-select": "^4.0.0-beta.6", "vue-select": "^4.0.0-beta.6",
"vue3-drr-grid-layout": "^1.9.7" "vue3-drr-grid-layout": "^1.9.7"
}, },
"devDependencies": { "devDependencies": {
"@cypress/vite-dev-server": "^5.0.5", "@cypress/vite-dev-server": "^5.0.5",
"@cypress/vue": "^5.0.5", "@cypress/vue": "^5.0.5",
"@iconify-json/mdi": "^1.1.52",
"@types/echarts": "^4.9.17", "@types/echarts": "^4.9.17",
"@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.59.6",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "3.2.47", "@vue/compiler-sfc": "3.3.2",
"@vue/eslint-config-prettier": "^7.1.0", "@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-dom": "^3.2.47", "@vue/runtime-dom": "^3.3.2",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.3.2",
"cypress": "^12.10.0", "cypress": "^12.12.0",
"eslint": "^8.38.0", "eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.11.0", "eslint-plugin-vue": "^9.13.0",
"mutationobserver-shim": "^0.3.7", "mutationobserver-shim": "^0.3.7",
"portal-vue": "^3.0.0", "portal-vue": "^3.0.0",
"prettier": "^2.8.7", "prettier": "^2.8.8",
"sass": "^1.62.0", "sass": "^1.62.1",
"typescript": "~5.0.4", "typescript": "~5.0.4",
"vite": "^4.2.2", "unplugin-icons": "^0.16.1",
"vitest": "^0.30.1", "unplugin-vue-components": "^0.24.1",
"vue-tsc": "^1.2.0" "vite": "^4.3.7",
"vitest": "^0.31.0",
"vue-tsc": "^1.6.5"
} }
} }

View File

@ -6,17 +6,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import NavBar from '@/components/layout/NavBar.vue'; import NavBar from '@/components/layout/NavBar.vue';
import NavFooter from '@/components/layout/NavFooter.vue'; import NavFooter from '@/components/layout/NavFooter.vue';
import BodyLayout from '@/components/layout/BodyLayout.vue'; import BodyLayout from '@/components/layout/BodyLayout.vue';
import { setTimezone } from './shared/formatters'; import { setTimezone } from './shared/formatters';
import { defineComponent, onMounted, watch } from 'vue'; import { onMounted, watch } from 'vue';
import { useSettingsStore } from './stores/settings'; import { useSettingsStore } from './stores/settings';
export default defineComponent({
name: 'App',
components: { NavBar, BodyLayout, NavFooter },
setup() {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
onMounted(() => { onMounted(() => {
setTimezone(settingsStore.timezone); setTimezone(settingsStore.timezone);
@ -28,9 +24,6 @@ export default defineComponent({
setTimezone(tz); setTimezone(tz);
}, },
); );
return {};
},
});
</script> </script>
<style scoped> <style scoped>

View File

@ -11,18 +11,18 @@
switch switch
@change="changeEvent" @change="changeEvent"
> >
<OnlineIcon <div
v-if="botStore.botStores[bot.botId].isBotLoggedIn" v-if="botStore.botStores[bot.botId].isBotLoggedIn"
:size="18" :title="botStore.botStores[bot.botId].isBotOnline ? 'Online' : 'Offline'"
>
<i-mdi-circle
class="ms-2 me-1 align-middle" class="ms-2 me-1 align-middle"
:class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'" :class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'"
:title="botStore.botStores[bot.botId].isBotOnline ? 'Online' : 'Offline'" />
></OnlineIcon> </div>
<LoggedOutIcon <div v-else title="Login info expired, please login again.">
v-else <i-mdi-cancel class="offline" />
class="offline" </div>
title="Login info expired, please login again."
></LoggedOutIcon>
</b-form-checkbox> </b-form-checkbox>
<div v-if="!noButtons" class="float-end d-flex flex-align-center"> <div v-if="!noButtons" class="float-end d-flex flex-align-center">
<b-button <b-button
@ -32,13 +32,13 @@
title="Edit bot" title="Edit bot"
@click="$emit('edit')" @click="$emit('edit')"
> >
<EditIcon :size="16" /> <i-mdi-pencil />
</b-button> </b-button>
<b-button v-else class="ms-1" size="sm" title="Login again" @click="$emit('editLogin')"> <b-button v-else class="ms-1" size="sm" title="Login again" @click="$emit('editLogin')">
<LoginIcon :size="16" /> <i-mdi-login />
</b-button> </b-button>
<b-button class="ms-1" size="sm" title="Delete bot" @click="botRemoveModalVisible = true"> <b-button class="ms-1" size="sm" title="Delete bot" @click="botRemoveModalVisible = true">
<DeleteIcon :size="16" title="Delete Bot" /> <i-mdi-delete />
</b-button> </b-button>
</div> </div>
</div> </div>
@ -54,31 +54,16 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import EditIcon from 'vue-material-design-icons/Pencil.vue';
import LoginIcon from 'vue-material-design-icons/Login.vue';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import OnlineIcon from 'vue-material-design-icons/Circle.vue';
import LoggedOutIcon from 'vue-material-design-icons/Cancel.vue';
import { BotDescriptor } from '@/types';
import { defineComponent, computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { BotDescriptor } from '@/types';
import { computed, ref } from 'vue';
export default defineComponent({ const props = defineProps({
name: 'BotEntry',
components: {
DeleteIcon,
EditIcon,
LoginIcon,
OnlineIcon,
LoggedOutIcon,
},
props: {
bot: { required: true, type: Object as () => BotDescriptor }, bot: { required: true, type: Object as () => BotDescriptor },
noButtons: { default: false, type: Boolean }, noButtons: { default: false, type: Boolean },
}, });
emits: ['edit', 'editLogin'], defineEmits(['edit', 'editLogin']);
setup(props) {
const botStore = useBotStore(); const botStore = useBotStore();
const changeEvent = (v) => { const changeEvent = (v) => {
@ -99,16 +84,6 @@ export default defineComponent({
// pass // pass
}, },
}); });
return {
botStore,
changeEvent,
autoRefreshLoc,
confirmRemoveBot,
botRemoveModalVisible,
};
},
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -13,7 +13,7 @@
class="d-flex" class="d-flex"
@click="botStore.selectBot(bot.botId)" @click="botStore.selectBot(bot.botId)"
> >
<ReorderIcon v-if="!small" class="handle me-2" /> <i-mdi-reorder-horizontal v-if="!small" class="handle me-2 fs-4" />
<bot-rename <bot-rename
v-if="editingBots.includes(bot.botId)" v-if="editingBots.includes(bot.botId)"
:bot="bot" :bot="bot"
@ -35,15 +35,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LoginModal from '@/views/LoginModal.vue';
import BotEntry from '@/components/BotEntry.vue'; import BotEntry from '@/components/BotEntry.vue';
import BotRename from '@/components/BotRename.vue'; import BotRename from '@/components/BotRename.vue';
import ReorderIcon from 'vue-material-design-icons/ReorderHorizontal.vue'; import LoginModal from '@/views/LoginModal.vue';
import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { AuthStorageWithBotId, BotDescriptor } from '@/types'; import { AuthStorageWithBotId, BotDescriptor } from '@/types';
import { useSortable } from '@vueuse/integrations/useSortable'; import { useSortable } from '@vueuse/integrations/useSortable';
import { computed, ref } from 'vue';
defineProps({ defineProps({
small: { default: false, type: Boolean }, small: { default: false, type: Boolean },

View File

@ -23,6 +23,14 @@
:state="urlState === '' ? null : urlState" :state="urlState === '' ? null : urlState"
@keydown.enter="handleOk" @keydown.enter="handleOk"
></b-form-input> ></b-form-input>
<b-alert
v-if="urlDuplicate"
class="mt-2 p-1 alert-wrap"
:model-value="true"
variant="warning"
>
This URL is already in use by another bot.
</b-alert>
</b-form-group> </b-form-group>
<b-form-group <b-form-group
:state="nameState" :state="nameState"
@ -78,7 +86,7 @@
import { useUserService } from '@/shared/userService'; import { useUserService } from '@/shared/userService';
import { AuthPayload, AuthStorageWithBotId } from '@/types'; import { AuthPayload, AuthStorageWithBotId } from '@/types';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
@ -113,10 +121,16 @@ const emitLoginResult = (value: boolean) => {
emit('loginResult', value); emit('loginResult', value);
}; };
const urlDuplicate = computed<boolean>(() => {
const bots = Object.values(botStore.availableBots).find((bot) => bot.botUrl === auth.value.url);
return bots !== undefined;
});
const checkFormValidity = () => { const checkFormValidity = () => {
const valid = formRef.value?.checkValidity(); const valid = formRef.value?.checkValidity();
nameState.value = valid || auth.value.username !== ''; nameState.value = valid || auth.value.username !== '';
pwdState.value = valid || auth.value.password !== ''; pwdState.value = valid || auth.value.password !== '';
urlState.value = valid || auth.value.url !== '';
return valid; return valid;
}; };

View File

@ -11,34 +11,25 @@
<div class="d-flex ms-2"> <div class="d-flex ms-2">
<b-button type="submit" size="sm" title="Save"> <b-button type="submit" size="sm" title="Save">
<CheckIcon :size="16" /> <i-mdi-check />
</b-button> </b-button>
<b-button class="ms-1" size="sm" title="Cancel" @click="$emit('cancelled')"> <b-button class="ms-1" size="sm" title="Cancel" @click="$emit('cancelled')">
<CloseIcon :size="16" /> <i-mdi-close />
</b-button> </b-button>
</div> </div>
</form> </form>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import CheckIcon from 'vue-material-design-icons/Check.vue';
import CloseIcon from 'vue-material-design-icons/Close.vue';
import { BotDescriptor } from '@/types';
import { defineComponent, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { BotDescriptor } from '@/types';
import { ref } from 'vue';
export default defineComponent({ const props = defineProps({
name: 'BotRename',
components: {
CheckIcon,
CloseIcon,
},
props: {
bot: { type: Object as () => BotDescriptor, required: true }, bot: { type: Object as () => BotDescriptor, required: true },
}, });
emits: ['cancelled', 'saved'], const emit = defineEmits(['cancelled', 'saved']);
setup(props, { emit }) {
const botStore = useBotStore(); const botStore = useBotStore();
const newName = ref<string>(props.bot.botName); const newName = ref<string>(props.bot.botName);
@ -50,10 +41,4 @@ export default defineComponent({
emit('saved'); emit('saved');
}; };
return {
newName,
save,
};
},
});
</script> </script>

View File

@ -1,13 +1,12 @@
<template> <template>
<b-link variant="outline-primary" class="nav-link" @click="toggleNight"> <b-link variant="outline-primary" class="nav-link" @click="toggleNight">
<ThemeLightDark :size="16" /> <i-mdi-brightness-6 />
</b-link> </b-link>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue';
import ThemeLightDark from 'vue-material-design-icons/Brightness6.vue';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { onMounted, ref } from 'vue';
const activeTheme = ref(''); const activeTheme = ref('');
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();

View File

@ -1,5 +1,5 @@
<template> <template>
<v-chart <e-charts
v-if="currencies" v-if="currencies"
:option="balanceChartOptions" :option="balanceChartOptions"
:theme="settingsStore.chartTheme" :theme="settingsStore.chartTheme"
@ -7,7 +7,7 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import ECharts from 'vue-echarts'; import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
@ -22,9 +22,9 @@ import {
TooltipComponent, TooltipComponent,
} from 'echarts/components'; } from 'echarts/components';
import { BalanceRecords } from '@/types'; import { BalanceValues } from '@/types';
import { formatPriceCurrency } from '@/shared/formatters'; import { formatPriceCurrency } from '@/shared/formatters';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
use([ use([
@ -37,16 +37,10 @@ use([
LabelLayout, LabelLayout,
]); ]);
export default defineComponent({ const props = defineProps({
name: 'BalanceChart', currencies: { required: true, type: Array as () => BalanceValues[] },
components: {
'v-chart': ECharts,
},
props: {
currencies: { required: true, type: Array as () => BalanceRecords[] },
showTitle: { required: false, type: Boolean }, showTitle: { required: false, type: Boolean },
}, });
setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const balanceChartOptions = computed((): EChartsOption => { const balanceChartOptions = computed((): EChartsOption => {
@ -95,10 +89,6 @@ export default defineComponent({
], ],
}; };
}); });
return { balanceChartOptions, settingsStore };
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,27 +1,34 @@
<template> <template>
<div class="d-flex flex-grow-1 chart-wrapper"> <div class="d-flex flex-grow-1 chart-wrapper">
<v-chart v-if="hasData" ref="candleChart" :theme="theme" autoresize manual-update /> <e-charts v-if="hasData" ref="candleChart" :theme="theme" autoresize manual-update />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, computed, onMounted, watch } from 'vue'; import { generateAreaCandleSeries, generateCandleSeries } from '@/shared/charts/candleChartSeries';
import { Trade, PairHistory, PlotConfig, ChartSliderPosition } from '@/types'; import heikinashi from '@/shared/charts/heikinashi';
import randomColor from '@/shared/randomColor';
import heikinashi from '@/shared/heikinashi';
import { getTradeEntries } from '@/shared/charts/tradeChartData'; import { getTradeEntries } from '@/shared/charts/tradeChartData';
import ECharts from 'vue-echarts'; import {
ChartSliderPosition,
ChartType,
IndicatorConfig,
PairHistory,
PlotConfig,
Trade,
} from '@/types';
import { format } from 'date-fns-tz'; import { format } from 'date-fns-tz';
import { computed, onMounted, ref, watch } from 'vue';
import ECharts from 'vue-echarts';
import { use } from 'echarts/core'; import { calculateDiff, getDiffColumnsFromPlotConfig } from '@/shared/charts/areaPlotDataset';
import { EChartsOption, SeriesOption, ScatterSeriesOption } from 'echarts'; import { dataZoomPartial } from '@/shared/charts/chartZoom';
import { CanvasRenderer } from 'echarts/renderers'; import { EChartsOption, ScatterSeriesOption } from 'echarts';
import { CandlestickChart, LineChart, BarChart, ScatterChart } from 'echarts/charts'; import { BarChart, CandlestickChart, LineChart, ScatterChart } from 'echarts/charts';
import { import {
AxisPointerComponent, AxisPointerComponent,
CalendarComponent, CalendarComponent,
DatasetComponent,
DataZoomComponent, DataZoomComponent,
DatasetComponent,
GridComponent, GridComponent,
LegendComponent, LegendComponent,
TimelineComponent, TimelineComponent,
@ -31,7 +38,8 @@ import {
VisualMapComponent, VisualMapComponent,
VisualMapPiecewiseComponent, VisualMapPiecewiseComponent,
} from 'echarts/components'; } from 'echarts/components';
import { dataZoomPartial } from '@/shared/charts/chartZoom'; import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
use([ use([
AxisPointerComponent, AxisPointerComponent,
@ -71,10 +79,7 @@ const shortEntrySignalColor = '#00ff26';
const sellSignalColor = '#faba25'; const sellSignalColor = '#faba25';
const shortexitSignalColor = '#faba25'; const shortexitSignalColor = '#faba25';
export default defineComponent({ const props = defineProps({
name: 'CandleChart',
components: { 'v-chart': ECharts },
props: {
trades: { required: false, default: () => [], type: Array as () => Trade[] }, trades: { required: false, default: () => [], type: Array as () => Trade[] },
dataset: { required: true, type: Object as () => PairHistory }, dataset: { required: true, type: Object as () => PairHistory },
heikinAshi: { required: false, default: false, type: Boolean }, heikinAshi: { required: false, default: false, type: Boolean },
@ -86,11 +91,8 @@ export default defineComponent({
type: Object as () => ChartSliderPosition, type: Object as () => ChartSliderPosition,
default: () => undefined, default: () => undefined,
}, },
}, });
setup(props) {
const candleChart = ref<typeof ECharts>(); const candleChart = ref<typeof ECharts>();
const buyData = ref<number[][]>([]);
const sellData = ref<number[][]>([]);
const chartOptions = ref<EChartsOption>({}); const chartOptions = ref<EChartsOption>({});
const strategy = computed(() => { const strategy = computed(() => {
@ -105,10 +107,6 @@ export default defineComponent({
return props.dataset ? props.dataset.timeframe : ''; return props.dataset ? props.dataset.timeframe : '';
}); });
const datasetColumns = computed(() => {
return props.dataset ? props.dataset.columns : [];
});
const hasData = computed(() => { const hasData = computed(() => {
return props.dataset !== null && typeof props.dataset === 'object'; return props.dataset !== null && typeof props.dataset === 'object';
}); });
@ -121,32 +119,34 @@ export default defineComponent({
return `${strategy.value} - ${pair.value} - ${timeframe.value}`; return `${strategy.value} - ${pair.value} - ${timeframe.value}`;
}); });
const updateChart = (initial = false) => { const diffCols = computed(() => {
return getDiffColumnsFromPlotConfig(props.plotConfig);
});
function updateChart(initial = false) {
if (!hasData.value) { if (!hasData.value) {
return; return;
} }
if (chartOptions.value?.title) { if (chartOptions.value?.title) {
chartOptions.value.title[0].text = chartTitle.value; chartOptions.value.title[0].text = chartTitle.value;
} }
const colDate = props.dataset.columns.findIndex((el) => el === '__date_ts'); const columns = props.dataset.columns;
const colOpen = props.dataset.columns.findIndex((el) => el === 'open');
const colHigh = props.dataset.columns.findIndex((el) => el === 'high'); const colDate = columns.findIndex((el) => el === '__date_ts');
const colLow = props.dataset.columns.findIndex((el) => el === 'low'); const colOpen = columns.findIndex((el) => el === 'open');
const colClose = props.dataset.columns.findIndex((el) => el === 'close'); const colHigh = columns.findIndex((el) => el === 'high');
const colVolume = props.dataset.columns.findIndex((el) => el === 'volume'); const colLow = columns.findIndex((el) => el === 'low');
const colEntryData = props.dataset.columns.findIndex( const colClose = columns.findIndex((el) => el === 'close');
const colVolume = columns.findIndex((el) => el === 'volume');
const colEntryData = columns.findIndex(
(el) => el === '_buy_signal_close' || el === '_enter_long_signal_close', (el) => el === '_buy_signal_close' || el === '_enter_long_signal_close',
); );
const colExitData = props.dataset.columns.findIndex( const colExitData = columns.findIndex(
(el) => el === '_sell_signal_close' || el === '_exit_long_signal_close', (el) => el === '_sell_signal_close' || el === '_exit_long_signal_close',
); );
const colShortEntryData = props.dataset.columns.findIndex( const colShortEntryData = columns.findIndex((el) => el === '_enter_short_signal_close');
(el) => el === '_enter_short_signal_close', const colShortExitData = columns.findIndex((el) => el === '_exit_short_signal_close');
);
const colShortExitData = props.dataset.columns.findIndex(
(el) => el === '_exit_short_signal_close',
);
const subplotCount = const subplotCount =
'subplots' in props.plotConfig ? Object.keys(props.plotConfig.subplots).length + 1 : 1; 'subplots' in props.plotConfig ? Object.keys(props.plotConfig.subplots).length + 1 : 1;
@ -170,13 +170,19 @@ export default defineComponent({
}); });
} }
} }
const dataset = props.heikinAshi let dataset = props.heikinAshi
? heikinashi(datasetColumns.value, props.dataset.data) ? heikinashi(columns, props.dataset.data)
: props.dataset.data.slice(); : props.dataset.data.slice();
diffCols.value.forEach(([colFrom, colTo]) => {
// Enhance dataset with diff columns for area plots
dataset = calculateDiff(columns, dataset, colFrom, colTo);
});
// Add new rows to end to allow slight "scroll past" // Add new rows to end to allow slight "scroll past"
const newArray = Array(dataset.length > 0 ? dataset[dataset.length - 2].length : 0); const newArray = Array(dataset.length > 0 ? dataset[dataset.length - 2].length : 0);
newArray[colDate] = dataset[dataset.length - 1][colDate] + props.dataset.timeframe_ms * 3; newArray[colDate] = dataset[dataset.length - 1][colDate] + props.dataset.timeframe_ms * 3;
dataset.push(newArray); dataset.push(newArray);
const options: EChartsOption = { const options: EChartsOption = {
dataset: { dataset: {
source: dataset, source: dataset,
@ -323,27 +329,28 @@ export default defineComponent({
if ('main_plot' in props.plotConfig) { if ('main_plot' in props.plotConfig) {
Object.entries(props.plotConfig.main_plot).forEach(([key, value]) => { Object.entries(props.plotConfig.main_plot).forEach(([key, value]) => {
const col = props.dataset.columns.findIndex((el) => el === key); const col = columns.findIndex((el) => el === key);
if (col > 1) { if (col > 0) {
if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) { if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
chartOptions.value.legend.data.push(key); 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)) { if (Array.isArray(chartOptions.value?.series)) {
chartOptions.value?.series.push(sp); chartOptions.value?.series.push(generateCandleSeries(colDate, col, key, value));
if (value.fill_to) {
// Assign
const fillColKey = `${key}-${value.fill_to}`;
const fillCol = columns.findIndex((el) => el === fillColKey);
const fillValue: IndicatorConfig = {
color: value.color,
type: ChartType.line,
};
const areaSeries = generateAreaCandleSeries(colDate, fillCol, key, fillValue, 0);
chartOptions.value.series[chartOptions.value.series.length - 1]['stack'] = key;
chartOptions.value.series.push(areaSeries);
}
chartOptions.value?.series.splice(chartOptions.value?.series.length - 1, 0);
} }
} else { } else {
console.log(`element ${key} for main plot not found in columns.`); console.log(`element ${key} for main plot not found in columns.`);
@ -360,10 +367,7 @@ export default defineComponent({
// Subplots are added from bottom to top - only the "bottom-most" plot stays at the bottom. // 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 = totalSubplots - plotIndex > 1 ? totalSubplots - plotIndex : plotIndex;
const currGridIdx = plotIndex; const currGridIdx = plotIndex;
if ( if (Array.isArray(chartOptions.value.yAxis) && chartOptions.value.yAxis.length <= plotIndex) {
Array.isArray(chartOptions.value.yAxis) &&
chartOptions.value.yAxis.length <= plotIndex
) {
chartOptions.value.yAxis.push({ chartOptions.value.yAxis.push({
scale: true, scale: true,
gridIndex: currGridIdx, gridIndex: currGridIdx,
@ -376,10 +380,7 @@ export default defineComponent({
splitLine: { show: false }, splitLine: { show: false },
}); });
} }
if ( if (Array.isArray(chartOptions.value.xAxis) && chartOptions.value.xAxis.length <= plotIndex) {
Array.isArray(chartOptions.value.xAxis) &&
chartOptions.value.xAxis.length <= plotIndex
) {
chartOptions.value.xAxis.push({ chartOptions.value.xAxis.push({
type: 'time', type: 'time',
scale: true, scale: true,
@ -410,27 +411,33 @@ export default defineComponent({
} }
Object.entries(value).forEach(([sk, sv]) => { Object.entries(value).forEach(([sk, sv]) => {
// entries per subplot // entries per subplot
const col = props.dataset.columns.findIndex((el) => el === sk); const col = columns.findIndex((el) => el === sk);
if (col > 0) { if (col > 0) {
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) { if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
chartOptions.value.legend.data.push(sk); 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)) { if (chartOptions.value.series && Array.isArray(chartOptions.value.series)) {
chartOptions.value.series.push(sp); chartOptions.value.series.push(generateCandleSeries(colDate, col, sk, sv, plotIndex));
if (sv.fill_to) {
// Assign
const fillColKey = `${sk}-${sv.fill_to}`;
const fillCol = columns.findIndex((el) => el === fillColKey);
const fillValue: IndicatorConfig = {
color: sv.color,
type: ChartType.line,
};
const areaSeries = generateAreaCandleSeries(
colDate,
fillCol,
sk,
fillValue,
plotIndex,
);
chartOptions.value.series[chartOptions.value.series.length - 1]['stack'] = sk;
chartOptions.value.series.push(areaSeries);
}
chartOptions.value?.series.splice(chartOptions.value?.series.length - 1, 0);
} }
} else { } else {
console.log(`element ${sk} was not found in the columns.`); console.log(`element ${sk} was not found in the columns.`);
@ -495,9 +502,9 @@ export default defineComponent({
replaceMerge: ['series', 'grid', 'yAxis', 'xAxis', 'legend'], replaceMerge: ['series', 'grid', 'yAxis', 'xAxis', 'legend'],
noMerge: !initial, noMerge: !initial,
}); });
}; }
const initializeChartOptions = () => { function initializeChartOptions() {
// Ensure we start empty. // Ensure we start empty.
candleChart.value?.setOption({}, { noMerge: true }); candleChart.value?.setOption({}, { noMerge: true });
@ -638,9 +645,9 @@ export default defineComponent({
console.log('Initialized'); console.log('Initialized');
updateChart(true); updateChart(true);
}; }
const updateSliderPosition = () => { function updateSliderPosition() {
if (!props.sliderPosition) return; if (!props.sliderPosition) return;
const start = format( const start = format(
@ -661,8 +668,10 @@ export default defineComponent({
endValue: end, endValue: end,
}); });
} }
}; }
// const buyData = ref<number[][]>([]);
// const sellData = ref<number[][]>([]);
// 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) {
@ -706,21 +715,6 @@ export default defineComponent({
() => props.sliderPosition, () => props.sliderPosition,
() => updateSliderPosition(), () => updateSliderPosition(),
); );
return {
candleChart,
buyData,
sellData,
strategy,
pair,
timeframe,
datasetColumns,
hasData,
filteredTrades,
chartTitle,
};
},
});
</script> </script>
<style scoped> <style scoped>

View File

@ -15,7 +15,9 @@
> >
</v-select> </v-select>
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">&#x21bb;</b-button> <b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">
<i-mdi-refresh />
</b-button>
<small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals" <small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals"
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small >Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
> >
@ -35,18 +37,12 @@
> >
<div class="ms-2"> <div class="ms-2">
<b-form-select <plot-config-select></plot-config-select>
v-model="plotStore.plotConfigName"
:options="plotStore.availablePlotConfigNames"
size="sm"
@change="plotStore.plotConfigChanged"
>
</b-form-select>
</div> </div>
<div class="ms-2 me-0 me-md-1"> <div class="ms-2 me-0 me-md-1">
<b-button size="sm" title="Plot configurator" @click="showConfigurator"> <b-button size="sm" title="Plot configurator" @click="showConfigurator">
&#9881; <i-mdi-cog width="12" height="12" />
</b-button> </b-button>
</div> </div>
</div> </div>
@ -91,15 +87,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Trade, PairHistory, LoadingStatus, ChartSliderPosition } from '@/types';
import CandleChart from '@/components/charts/CandleChart.vue'; import CandleChart from '@/components/charts/CandleChart.vue';
import PlotConfigSelect from '@/components/charts/PlotConfigSelect.vue';
import PlotConfigurator from '@/components/charts/PlotConfigurator.vue'; import PlotConfigurator from '@/components/charts/PlotConfigurator.vue';
import vSelect from 'vue-select';
import { useSettingsStore } from '@/stores/settings';
import { usePlotConfigStore } from '@/stores/plotConfig'; import { usePlotConfigStore } from '@/stores/plotConfig';
import { useSettingsStore } from '@/stores/settings';
import { ChartSliderPosition, LoadingStatus, PairHistory, Trade } from '@/types';
import vSelect from 'vue-select';
import { ref, computed, onMounted, watch } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { computed, onMounted, ref, watch } from 'vue';
const props = defineProps({ const props = defineProps({
trades: { required: false, default: () => [], type: Array as () => Trade[] }, trades: { required: false, default: () => [], type: Array as () => Trade[] },
@ -111,6 +108,7 @@ const props = defineProps({
timerange: { required: false, default: '', type: String }, timerange: { required: false, default: '', type: String },
/** Only required if historicView is true */ /** Only required if historicView is true */
strategy: { required: false, default: '', type: String }, strategy: { required: false, default: '', type: String },
freqaiModel: { required: false, default: undefined, type: String },
sliderPosition: { sliderPosition: {
required: false, required: false,
type: Object as () => ChartSliderPosition, type: Object as () => ChartSliderPosition,
@ -176,6 +174,7 @@ const refresh = () => {
timeframe: props.timeframe, timeframe: props.timeframe,
timerange: props.timerange, timerange: props.timerange,
strategy: props.strategy, strategy: props.strategy,
freqaimodel: props.freqaiModel,
}); });
} else { } else {
botStore.activeBot.getPairCandles({ botStore.activeBot.getPairCandles({

View File

@ -1,8 +1,8 @@
<template> <template>
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" /> <e-charts v-if="trades" ref="chart" autoresize manual-update :theme="settingsStore.chartTheme" />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import ECharts from 'vue-echarts'; import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
@ -17,10 +17,20 @@ import {
TooltipComponent, TooltipComponent,
} from 'echarts/components'; } from 'echarts/components';
import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types'; import {
import { defineComponent, computed, ComputedRef } from 'vue'; ClosedTrade,
CumProfitData,
CumProfitDataPerDate,
CumProfitChartData,
Trade,
} from '@/types';
import { computed } from 'vue';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { dataZoomPartial } from '@/shared/charts/chartZoom'; import { dataZoomPartial } from '@/shared/charts/chartZoom';
import { ref } from 'vue';
import { onMounted } from 'vue';
import { watch } from 'vue';
import { watchThrottled } from '@vueuse/core';
use([ use([
BarChart, BarChart,
@ -38,22 +48,23 @@ use([
// Define Column labels here to avoid typos // Define Column labels here to avoid typos
const CHART_PROFIT = 'Profit'; const CHART_PROFIT = 'Profit';
export default defineComponent({ const props = defineProps({
name: 'CumProfitChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => ClosedTrade[] }, trades: { required: true, type: Array as () => ClosedTrade[] },
openTrades: { required: true, type: Array as () => Trade[] },
showTitle: { default: true, type: Boolean }, showTitle: { default: true, type: Boolean },
profitColumn: { default: 'profit_abs', type: String }, profitColumn: { default: 'profit_abs', type: String },
}, });
setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
// const botList = ref<string[]>([]); // const botList = ref<string[]>([]);
// const cumulativeData = ref<{ date: number; profit: any }[]>([]); // const cumulativeData = ref<{ date: number; profit: any }[]>([]);
const cumulativeData: ComputedRef<{ date: number; profit: number }[]> = computed(() => { const chart = ref<typeof ECharts>();
const openProfit = computed<number>(() => {
return props.openTrades.reduce((a, v) => a + v[props.profitColumn], 0);
});
const cumulativeData = computed<CumProfitChartData[]>(() => {
const res: CumProfitData[] = []; const res: CumProfitData[] = [];
const resD: CumProfitDataPerDate = {}; const resD: CumProfitDataPerDate = {};
const closedTrades = props.trades const closedTrades = props.trades
@ -81,29 +92,111 @@ export default defineComponent({
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit }); res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
} }
} }
// console.log(resD);
return Object.entries(resD).map(([k, v]) => { const valueArray: CumProfitChartData[] = Object.entries(resD).map(
([k, v]: [string, CumProfitData]) => {
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) => {
// obj[botId] = v[botId]; // obj[botId] = v[botId];
// }); // });
return obj; return obj;
}); },
);
if (props.openTrades.length > 0 && valueArray.length > 0) {
const lastPoint = valueArray[valueArray.length - 1];
if (lastPoint) {
const resultWitHOpen = (lastPoint.profit ?? 0) + openProfit.value;
valueArray.push({ date: lastPoint.date, currentProfit: lastPoint.profit });
// Add one day to date to ensure it's showing properly
const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
valueArray.push({ date: tomorrow, currentProfit: resultWitHOpen });
}
}
return valueArray;
}); });
const chartOptions = computed((): EChartsOption => { function updateChart(initial = false) {
const chartOptionsLoc: EChartsOption = {
dataset: {
dimensions: ['date', 'profit', 'currentProfit'],
source: cumulativeData.value,
},
series: [
{
// Keep current-profit before profit, so the starting symbol is behind
type: 'line',
name: 'currentProfit',
animation: initial,
tooltip: {
show: false,
},
lineStyle: {
color: openProfit.value > 0 ? 'green' : 'red',
type: 'dotted',
},
itemStyle: {
color: openProfit.value > 0 ? 'green' : 'red',
},
encode: {
x: 'date',
y: 'currentProfit',
},
},
{
type: 'line',
name: CHART_PROFIT,
animation: initial,
step: 'end',
lineStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
itemStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
encode: {
x: 'date',
y: 'profit',
},
// symbol: 'none',
},
],
};
// TODO: maybe have profit lines per bot?
// this.botList.forEach((botId: string) => {
// console.log('bot', botId);
// chartOptionsLoc.series.push({
// type: 'line',
// name: botId,
// animation: true,
// step: 'end',
// lineStyle: {
// color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// itemStylesettingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// // symbol: 'none',
// });
// });
chart.value?.setOption(chartOptionsLoc, {
replaceMerge: ['series', 'dataset'],
noMerge: !initial,
});
}
function initializeChart() {
chart.value?.setOption({}, { noMerge: true });
const chartOptionsLoc: EChartsOption = { const chartOptionsLoc: EChartsOption = {
title: { title: {
text: 'Cumulative Profit', text: 'Cumulative Profit',
show: props.showTitle, show: props.showTitle,
}, },
backgroundColor: 'rgba(0, 0, 0, 0)', backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'profit'],
source: cumulativeData.value,
},
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
@ -152,44 +245,29 @@ export default defineComponent({
...dataZoomPartial, ...dataZoomPartial,
}, },
], ],
series: [
{
type: 'line',
name: CHART_PROFIT,
animation: true,
step: 'end',
lineStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
itemStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
// symbol: 'none',
},
],
}; };
// TODO: maybe have profit lines per bot? chart.value?.setOption(chartOptionsLoc, { noMerge: true });
// this.botList.forEach((botId: string) => { updateChart(true);
// console.log('bot', botId); }
// chartOptionsLoc.series.push({
// type: 'line', onMounted(() => {
// name: botId, initializeChart();
// animation: true,
// step: 'end',
// lineStyle: {
// color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// itemStylesettingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// // symbol: 'none',
// });
// });
return chartOptionsLoc;
}); });
return { settingsStore, cumulativeData, chartOptions }; watchThrottled(
() => props.openTrades,
() => {
updateChart();
}, },
}); { throttle: 60 * 1000 },
);
watchThrottled(
() => props.trades,
() => {
updateChart();
},
{ throttle: 60 * 1000 },
);
</script> </script>
<style scoped> <style scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<v-chart <e-charts
v-if="dailyStats.data" v-if="dailyStats.data"
:option="dailyChartOptions" :option="dailyChartOptions"
:theme="settingsStore.chartTheme" :theme="settingsStore.chartTheme"
@ -7,8 +7,8 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, computed, ComputedRef } from 'vue'; import { computed, ComputedRef } from 'vue';
import ECharts from 'vue-echarts'; import ECharts from 'vue-echarts';
// import { EChartsOption } from 'echarts'; // import { EChartsOption } from 'echarts';
@ -44,11 +44,7 @@ use([
const CHART_ABS_PROFIT = 'Absolute profit'; const CHART_ABS_PROFIT = 'Absolute profit';
const CHART_TRADE_COUNT = 'Trade Count'; const CHART_TRADE_COUNT = 'Trade Count';
export default defineComponent({ const props = defineProps({
components: {
'v-chart': ECharts,
},
props: {
dailyStats: { dailyStats: {
type: Object as () => DailyReturnValue, type: Object as () => DailyReturnValue,
required: true, required: true,
@ -57,9 +53,8 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, });
setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const absoluteMin = computed(() => const absoluteMin = computed(() =>
props.dailyStats.data.reduce( props.dailyStats.data.reduce(
@ -157,13 +152,6 @@ export default defineComponent({
], ],
}; };
}); });
return {
dailyChartOptions,
settingsStore,
};
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<v-chart <e-charts
v-if="trades.length > 0" v-if="trades.length > 0"
:option="hourlyChartOptions" :option="hourlyChartOptions"
autoresize autoresize
@ -7,10 +7,10 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import ECharts from 'vue-echarts'; import ECharts from 'vue-echarts';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { Trade } from '@/types'; import { Trade } from '@/types';
import { timestampHour } from '@/shared/formatters'; import { timestampHour } from '@/shared/formatters';
@ -45,16 +45,10 @@ use([
const CHART_PROFIT = 'Profit %'; const CHART_PROFIT = 'Profit %';
const CHART_TRADE_COUNT = 'Trade Count'; const CHART_TRADE_COUNT = 'Trade Count';
export default defineComponent({ const props = defineProps({
name: 'HourlyChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => Trade[] }, trades: { required: true, type: Array as () => Trade[] },
showTitle: { default: true, type: Boolean }, showTitle: { default: true, type: Boolean },
}, });
setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const hourlyData = computed(() => { const hourlyData = computed(() => {
@ -158,9 +152,6 @@ export default defineComponent({
], ],
}; };
}); });
return { settingsStore, hourlyChartOptions };
},
});
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,38 @@
<template>
<edit-value
v-model="plotStore.plotConfigName"
:allow-edit="allowEdit"
:allow-add="allowEdit"
editable-name="plot configuration"
@rename="plotStore.renamePlotConfig"
@delete="plotStore.deletePlotConfig"
@new="plotStore.newPlotConfig"
>
<b-form-select
v-model="plotStore.plotConfigName"
:options="plotStore.availablePlotConfigNames"
size="sm"
@change="plotStore.plotConfigChanged"
>
</b-form-select>
</edit-value>
</template>
<script setup lang="ts">
import EditValue from '@/components/general/EditValue.vue';
import { usePlotConfigStore } from '@/stores/plotConfig';
defineProps({
allowEdit: {
type: Boolean,
default: false,
},
editableName: {
type: String,
default: 'plot configuration',
},
});
const plotStore = usePlotConfigStore();
</script>
<style scoped></style>

View File

@ -1,120 +1,147 @@
<template> <template>
<div v-if="columns"> <div v-if="columns">
<b-form-group label="Plot config name" label-for="idPlotConfigName"> <b-form-group label="Plot config name" label-for="idPlotConfigName">
<b-form-input id="idPlotConfigName" v-model="plotConfigNameLoc" size="sm"> </b-form-input> <plot-config-select allow-edit></plot-config-select>
</b-form-group> </b-form-group>
<div class="col-mb-3"> <div class="col-mb-3">
<hr /> <hr />
<b-form-group label="Target Plot" label-for="FieldSel">
<b-form-group label="Target" label-for="FieldSel"> <edit-value
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="3"> v-model="selSubPlot"
:allow-edit="!isMainPlot"
allow-add
editable-name="plot configuration"
align-vertical
@new="addSubplot"
@delete="deleteSubplot"
@rename="renameSubplot"
>
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="5">
</b-form-select> </b-form-select>
</edit-value>
</b-form-group> </b-form-group>
</div> </div>
<b-form-group label="Add new plot" label-for="newSubPlot">
<b-input-group size="sm">
<b-form-input id="newSubPlot" v-model="newSubplotName" class="addPlot"></b-form-input>
<b-input-group-append>
<b-button :disabled="!newSubplotName" @click="addSubplot">+</b-button>
<b-button v-if="selSubPlot && selSubPlot != 'main_plot'" @click="delSubplot">-</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<hr /> <hr />
<div> <div>
<b-form-group label="Used indicators" label-for="selectedIndicators"> <b-form-group label="Indicators in this plot" label-for="selectedIndicators">
<b-form-select <b-form-select
id="selectedIndicators" id="selectedIndicators"
v-model="selIndicatorName" v-model="selIndicatorName"
:disabled="addNewIndicator"
:options="usedColumns" :options="usedColumns"
:select-size="4" :select-size="4"
> >
</b-form-select> </b-form-select>
</b-form-group> </b-form-group>
</div> </div>
<div> <div class="d-flex flex-row mt-1">
<b-button
variant="primary"
title="Add indicator to plot"
size="sm"
:disabled="addNewIndicator"
@click="addNewIndicator = !addNewIndicator"
>
Add new indicator
</b-button>
<b-button <b-button
variant="secondary" variant="secondary"
title="Remove indicator to plot" title="Remove indicator to plot"
size="sm" size="sm"
:disabled="!selIndicatorName" :disabled="!selIndicatorName"
class="ms-1" class="col"
@click="removeIndicator" @click="removeIndicator"
> >
Remove indicator Remove indicator
</b-button> </b-button>
<b-button
variant="primary"
title="Add indicator to plot"
size="sm"
class="ms-1 col"
:disabled="addNewIndicator"
@click="
addNewIndicator = !addNewIndicator;
selIndicatorName = '';
"
>
Add new indicator
</b-button>
</div> </div>
<PlotIndicator <PlotIndicatorSelect
v-if="selIndicatorName || addNewIndicator" v-if="addNewIndicator"
:columns="columns"
class="mt-1"
label="Select indicator to add"
@indicator-selected="addNewIndicatorSelected"
/>
<plot-indicator
v-if="selIndicatorName"
v-model="selIndicator" v-model="selIndicator"
class="mt-1" class="mt-1"
:columns="columns" :columns="columns"
:add-new="addNewIndicator"
/> />
<hr /> <hr />
<div> <div class="d-flex flex-row">
<b-button class="ms-1" variant="secondary" size="sm" @click="loadPlotConfig">Load</b-button> <b-button
class="ms-1 col"
variant="secondary"
size="sm"
:disabled="addNewIndicator"
title="Reset to last saved configuration"
@click="loadPlotConfig"
>Reset</b-button
>
<!--
Does Resetting a config to "nothing" make sense, or can this be done via "delete / create"?
<b-button
class="ms-1 col"
variant="secondary"
size="sm"
:disabled="addNewIndicator"
title="Start with empty configuration"
@click="clearConfig"
>Reset</b-button
> -->
<b-button <b-button
:disabled=" :disabled="
(botStore.activeBot.isWebserverMode && botStore.activeBot.botApiVersion < 2.23) || (botStore.activeBot.isWebserverMode && botStore.activeBot.botApiVersion < 2.23) ||
!botStore.activeBot.isBotOnline !botStore.activeBot.isBotOnline ||
addNewIndicator
" "
class="ms-1" class="ms-1 col"
variant="secondary" variant="secondary"
size="sm" size="sm"
@click="loadPlotConfigFromStrategy" @click="loadPlotConfigFromStrategy"
> >
From strategy From strategy
</b-button> </b-button>
<b-button
class="ms-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@click="resetConfig"
>Reset</b-button
>
<b-button <b-button
id="showButton" id="showButton"
class="ms-1" class="ms-1 col"
variant="secondary" variant="secondary"
size="sm" size="sm"
:disabled="addNewIndicator"
title="Show configuration for easy transfer to a strategy" title="Show configuration for easy transfer to a strategy"
@click="showConfig = !showConfig" @click="showConfig = !showConfig"
>Show</b-button >{{ showConfig ? 'Hide' : 'Show' }}</b-button
> >
<b-button
class="ms-1 col"
variant="primary"
size="sm"
data-toggle="tooltip"
:disabled="addNewIndicator"
title="Save configuration"
@click="savePlotConfig"
>Save</b-button
>
</div>
<b-button <b-button
v-if="showConfig" v-if="showConfig"
class="ms-1" class="ms-1 mt-1"
variant="secondary" variant="secondary"
size="sm" size="sm"
title="Load configuration from text box below" title="Load configuration from text box below"
@click="loadConfigFromString" @click="loadConfigFromString"
>Load from String</b-button >Load from String</b-button
> >
<b-button
class="ms-1"
variant="primary"
size="sm"
data-toggle="tooltip"
title="Save configuration"
@click="savePlotConfig"
>Save</b-button
>
</div>
<div v-if="showConfig" class="col-mb-5 ms-1 mt-2"> <div v-if="showConfig" class="col-mb-5 ms-1 mt-2">
<b-form-textarea <b-form-textarea
id="TextArea" id="TextArea"
@ -129,14 +156,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PlotConfig, EMPTY_PLOTCONFIG, IndicatorConfig } from '@/types'; import EditValue from '@/components/general/EditValue.vue';
import { getCustomPlotConfig } from '@/shared/storage'; import PlotConfigSelect from '@/components/charts/PlotConfigSelect.vue';
import PlotIndicator from '@/components/charts/PlotIndicator.vue'; import PlotIndicator from '@/components/charts/PlotIndicator.vue';
import { showAlert } from '@/stores/alerts'; import { showAlert } from '@/stores/alerts';
import { IndicatorConfig, PlotConfig } from '@/types';
import PlotIndicatorSelect from './PlotIndicatorSelect.vue';
import { computed, ref, onMounted } from 'vue'; import { deepClone } from '@/shared/deepClone';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { usePlotConfigStore } from '@/stores/plotConfig'; import { usePlotConfigStore } from '@/stores/plotConfig';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import randomColor from '@/shared/randomColor';
defineProps({ defineProps({
columns: { required: true, type: Array as () => string[] }, columns: { required: true, type: Array as () => string[] },
@ -146,10 +177,7 @@ defineProps({
const plotStore = usePlotConfigStore(); const plotStore = usePlotConfigStore();
const botStore = useBotStore(); const botStore = useBotStore();
const plotConfig = ref<PlotConfig>(EMPTY_PLOTCONFIG);
const plotConfigNameLoc = ref('default'); const plotConfigNameLoc = ref('default');
const newSubplotName = ref('');
const selIndicatorName = ref(''); const selIndicatorName = ref('');
const addNewIndicator = ref(false); const addNewIndicator = ref(false);
const showConfig = ref(false); const showConfig = ref(false);
@ -163,43 +191,40 @@ const isMainPlot = computed(() => {
const currentPlotConfig = computed(() => { const currentPlotConfig = computed(() => {
if (isMainPlot.value) { if (isMainPlot.value) {
return plotConfig.value.main_plot; return plotStore.editablePlotConfig.main_plot;
} }
return plotConfig.value.subplots[selSubPlot.value]; return plotStore.editablePlotConfig.subplots[selSubPlot.value];
}); });
const subplots = computed((): string[] => { const subplots = computed((): string[] => {
// Subplot keys (for selection window) // Subplot keys (for selection window)
return ['main_plot', ...Object.keys(plotConfig.value.subplots)]; return ['main_plot', ...Object.keys(plotStore.editablePlotConfig.subplots)];
}); });
const usedColumns = computed((): string[] => { const usedColumns = computed((): string[] => {
if (isMainPlot.value) { if (isMainPlot.value) {
return Object.keys(plotConfig.value.main_plot); return Object.keys(plotStore.editablePlotConfig.main_plot);
} }
if (selSubPlot.value in plotConfig.value.subplots) { if (selSubPlot.value in plotStore.editablePlotConfig.subplots) {
return Object.keys(plotConfig.value.subplots[selSubPlot.value]); return Object.keys(plotStore.editablePlotConfig.subplots[selSubPlot.value]);
} }
return []; return [];
}); });
function addIndicator(newIndicator: Record<string, IndicatorConfig>) { function addIndicator(newIndicator: Record<string, IndicatorConfig>) {
console.log(plotConfig.value);
// const { plotConfig.value } = this; // const { plotConfig.value } = this;
const name = Object.keys(newIndicator)[0]; const name = Object.keys(newIndicator)[0];
const indicator = newIndicator[name]; const indicator = newIndicator[name];
if (isMainPlot.value) { if (isMainPlot.value) {
console.log(`Adding ${name} to MainPlot`); // console.log(`Adding ${name} to MainPlot`);
plotConfig.value.main_plot[name] = { ...indicator }; plotStore.editablePlotConfig.main_plot[name] = { ...indicator };
} else { } else {
console.log(`Adding ${name} to ${selSubPlot.value}`); // console.log(`Adding ${name} to ${selSubPlot.value}`);
plotConfig.value.subplots[selSubPlot.value][name] = { ...indicator }; plotStore.editablePlotConfig.subplots[selSubPlot.value][name] = { ...indicator };
} }
plotConfig.value = { ...plotConfig.value }; plotStore.editablePlotConfig = { ...plotStore.editablePlotConfig };
// Reset random color // Reset random color
addNewIndicator.value = false; addNewIndicator.value = false;
plotStore.setPlotConfig(plotConfig.value);
} }
const selIndicator = computed({ const selIndicator = computed({
@ -215,7 +240,6 @@ const selIndicator = computed({
return {}; return {};
}, },
set(newValue: Record<string, IndicatorConfig>) { set(newValue: Record<string, IndicatorConfig>) {
// console.log('newValue', newValue);
const name = Object.keys(newValue)[0]; const name = Object.keys(newValue)[0];
// this.currentPlotConfig[this.selIndicatorName] = { ...newValue[name] }; // this.currentPlotConfig[this.selIndicatorName] = { ...newValue[name] };
// this.emitPlotConfig(); // this.emitPlotConfig();
@ -229,7 +253,7 @@ const selIndicator = computed({
const plotConfigJson = computed({ const plotConfigJson = computed({
get() { get() {
return JSON.stringify(plotConfig.value, null, 2); return JSON.stringify(plotStore.editablePlotConfig, null, 2);
}, },
set(newValue: string) { set(newValue: string) {
try { try {
@ -243,55 +267,53 @@ const plotConfigJson = computed({
}); });
function removeIndicator() { function removeIndicator() {
console.log(plotConfig.value);
// const { plotConfig } = this;
if (isMainPlot.value) { if (isMainPlot.value) {
console.log(`Removing ${selIndicatorName.value} from MainPlot`); console.log(`Removing ${selIndicatorName.value} from MainPlot`);
delete plotConfig.value.main_plot[selIndicatorName.value]; delete plotStore.editablePlotConfig.main_plot[selIndicatorName.value];
} else { } else {
console.log(`Removing ${selIndicatorName.value} from ${selSubPlot.value}`); console.log(`Removing ${selIndicatorName.value} from ${selSubPlot.value}`);
delete plotConfig.value.subplots[selSubPlot.value][selIndicatorName.value]; delete plotStore.editablePlotConfig.subplots[selSubPlot.value][selIndicatorName.value];
} }
plotConfig.value = { ...plotConfig.value }; plotStore.editablePlotConfig = { ...plotStore.editablePlotConfig };
console.log(plotConfig.value);
selIndicatorName.value = ''; selIndicatorName.value = '';
plotStore.setPlotConfig(plotConfig.value);
} }
function addSubplot() { function addSubplot(newSubplotName: string) {
plotConfig.value.subplots = { plotStore.editablePlotConfig.subplots = {
...plotConfig.value.subplots, ...plotStore.editablePlotConfig.subplots,
[newSubplotName.value]: {}, [newSubplotName]: {},
}; };
selSubPlot.value = newSubplotName.value; selSubPlot.value = newSubplotName;
newSubplotName.value = '';
plotStore.setPlotConfig(plotConfig.value);
} }
function delSubplot() { function deleteSubplot(subplotName: string) {
delete plotConfig.value.subplots[selSubPlot.value]; delete plotStore.editablePlotConfig.subplots[subplotName];
plotConfig.value.subplots = { ...plotConfig.value.subplots }; // plotStore.editablePlotConfig.subplots = { ...plotStore.editablePlotConfig.subplots };
selSubPlot.value = ''; selSubPlot.value = subplots.value[subplots.value.length - 1];
plotStore.setPlotConfig(plotConfig.value);
} }
function renameSubplot(oldName: string, newName: string) {
plotStore.editablePlotConfig.subplots[newName] = plotStore.editablePlotConfig.subplots[oldName];
delete plotStore.editablePlotConfig.subplots[oldName];
selSubPlot.value = newName;
}
function loadPlotConfig() { function loadPlotConfig() {
plotConfig.value = getCustomPlotConfig(plotConfigNameLoc.value); // Reset from store
console.log(plotConfig.value); plotStore.editablePlotConfig = deepClone(plotStore.customPlotConfigs[plotStore.plotConfigName]);
console.log('loading config');
plotStore.setPlotConfig(plotConfig.value);
} }
function loadConfigFromString() { function loadConfigFromString() {
// this.plotConfig = JSON.parse();
if (tempPlotConfig.value !== undefined && tempPlotConfigValid.value) { if (tempPlotConfig.value !== undefined && tempPlotConfigValid.value) {
plotConfig.value = tempPlotConfig.value; plotStore.editablePlotConfig = tempPlotConfig.value;
plotStore.setPlotConfig(plotConfig.value);
} }
} }
function resetConfig() {
plotConfig.value = { ...EMPTY_PLOTCONFIG }; // function clearConfig() {
} // // Use empty config
// plotStore.editablePlotConfig = { ...EMPTY_PLOTCONFIG };
// }
async function loadPlotConfigFromStrategy() { async function loadPlotConfigFromStrategy() {
if (botStore.activeBot.isWebserverMode && !botStore.activeBot.strategy.strategy) { if (botStore.activeBot.isWebserverMode && !botStore.activeBot.strategy.strategy) {
showAlert(`No strategy selected, can't load plot config.`); showAlert(`No strategy selected, can't load plot config.`);
@ -300,8 +322,7 @@ async function loadPlotConfigFromStrategy() {
try { try {
await botStore.activeBot.getStrategyPlotConfig(); await botStore.activeBot.getStrategyPlotConfig();
if (botStore.activeBot.strategyPlotConfig) { if (botStore.activeBot.strategyPlotConfig) {
plotConfig.value = botStore.activeBot.strategyPlotConfig; plotStore.editablePlotConfig = botStore.activeBot.strategyPlotConfig;
plotStore.setPlotConfig(plotConfig.value);
} }
} catch (data) { } catch (data) {
// //
@ -310,14 +331,45 @@ async function loadPlotConfigFromStrategy() {
} }
function savePlotConfig() { function savePlotConfig() {
plotStore.saveCustomPlotConfig({ [plotConfigNameLoc.value]: plotConfig.value }); plotStore.saveCustomPlotConfig(plotConfigNameLoc.value, plotStore.editablePlotConfig);
} }
function addNewIndicatorSelected(indicator?: string) {
addNewIndicator.value = false;
if (indicator) {
addIndicator({
[indicator]: {
color: randomColor(),
},
});
selIndicatorName.value = indicator;
}
}
watch(selSubPlot, () => {
// Deselect Indicator when switching selected plot
selIndicatorName.value = '';
});
watch(
() => plotStore.plotConfigName,
() => {
selIndicatorName.value = '';
// selSubPlot.value = '';
},
);
onMounted(() => { onMounted(() => {
// console.log('Config Mounted', props); // Deep clone and assign to editable
plotConfig.value = plotStore.plotConfig; plotStore.editablePlotConfig = deepClone(plotStore.plotConfig);
plotStore.isEditing = true;
plotConfigNameLoc.value = plotStore.plotConfigName; plotConfigNameLoc.value = plotStore.plotConfigName;
}); });
onUnmounted(() => {
// TODO: Unmounted is not called when closing in Chart view
plotStore.isEditing = false;
});
</script> </script>
<style scoped> <style scoped>

View File

@ -1,133 +1,89 @@
<template> <template>
<div> <div>
<div v-if="addNew"> <div class="d-flex flex-col flex-xl-row justify-content-between mt-1">
<b-form-group label="Add indicator" label-for="indicatorSelector"> <b-form-group class="col flex-grow-1" label="Type" label-for="plotTypeSelector">
<b-input-group size="sm">
<b-form-input v-model="indicatorFilter" placeholder="Filter indicators"></b-form-input>
<b-input-group-append>
<Reset
class="pointer align-self-center ms-1"
:size="18"
@click="indicatorFilter = ''"
></Reset>
</b-input-group-append>
</b-input-group>
<b-form-select
id="indicatorSelector"
v-model="selAvailableIndicator"
:options="filteredIndicators"
:select-size="4"
>
</b-form-select>
</b-form-group>
</div>
<b-form-group label="Type" label-for="plotTypeSelector">
<b-form-select <b-form-select
id="plotTypeSelector" id="plotTypeSelector"
v-model="graphType" v-model="graphType"
size="sm" size="sm"
:options="availableGraphTypes" :options="availableGraphTypes"
@change="emitIndicator()"
> >
</b-form-select> </b-form-select>
</b-form-group> </b-form-group>
<hr /> <b-form-group label="Color" label-for="colsel" size="sm" class="ms-xl-1 col">
<b-form-group label="Color" label-for="colsel" size="sm">
<b-input-group> <b-input-group>
<b-input-group-prepend> <b-input-group-prepend>
<div :style="{ 'background-color': selColor }" class="colorbox me-2"></div> <b-form-input
<!-- <b-form-input
id="colsel"
v-model="selColor" v-model="selColor"
size="sm"
class="colorbox"
type="color" type="color"
:style="{ 'background-color': selColor }" size="sm"
> class="p-0"
</b-form-input> --> style="max-width: 29px"
></b-form-input>
</b-input-group-prepend> </b-input-group-prepend>
<b-form-input id="colsel" v-model="selColor" size="sm" class="flex-grow-1">
<b-form-input id="colsel" v-model="selColor" size="sm"> </b-form-input> </b-form-input>
<b-input-group-append> <b-input-group-append>
<b-button variant="primary" size="sm" @click="newColor">&#x21bb;</b-button> <b-button variant="primary" size="sm" @click="newColor">
<i-mdi-dice-multiple />
</b-button>
</b-input-group-append> </b-input-group-append>
</b-input-group> </b-input-group>
</b-form-group> </b-form-group>
<div class="d-flex d-flex-columns">
<b-button
v-if="addNew"
class="flex-grow-1"
variant="primary"
title="Add "
size="sm"
@click="emitIndicator"
>
Save indicator
</b-button>
<b-button
v-if="addNew"
class="ms-1 flex-grow-1"
variant="secondary"
title="Add "
size="sm"
@click="clickCancel"
>
Cancel
</b-button>
</div> </div>
<PlotIndicatorSelect
v-if="graphType === ChartType.line"
v-model="fillTo"
:columns="columns"
class="mt-1"
label="Select indicator to add"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ChartType, IndicatorConfig } from '@/types'; import { ChartType, IndicatorConfig } from '@/types';
import randomColor from '@/shared/randomColor'; import randomColor from '@/shared/randomColor';
import Reset from 'vue-material-design-icons/CloseCircleOutline.vue'; import PlotIndicatorSelect from '@/components/charts/PlotIndicatorSelect.vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { watchDebounced } from '@vueuse/core';
const props = defineProps({ const props = defineProps({
modelValue: { required: true, type: Object as () => Record<string, IndicatorConfig> }, modelValue: { required: true, type: Object as () => Record<string, IndicatorConfig> },
columns: { required: true, type: Array as () => string[] }, columns: { required: true, type: Array as () => string[] },
addNew: { required: true, type: Boolean },
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const selColor = ref(randomColor()); const selColor = ref(randomColor());
const graphType = ref<ChartType>(ChartType.line); const graphType = ref<ChartType>(ChartType.line);
const availableGraphTypes = ref(Object.keys(ChartType)); const availableGraphTypes = ref(Object.keys(ChartType));
const indicatorFilter = ref('');
const selAvailableIndicator = ref(''); const selAvailableIndicator = ref('');
const cancelled = ref(false); const cancelled = ref(false);
const fillTo = ref('');
const filteredIndicators = computed(() => { function newColor() {
return props.columns.filter((col) =>
col.toLowerCase().includes(indicatorFilter.value.toLowerCase()),
);
});
const newColor = () => {
selColor.value = randomColor(); selColor.value = randomColor();
}; }
const combinedIndicator = computed(() => { const combinedIndicator = computed<IndicatorConfig>(() => {
if (cancelled.value || !selAvailableIndicator.value) { if (cancelled.value || !selAvailableIndicator.value) {
return {}; return {};
} }
return { const val: IndicatorConfig = {
[selAvailableIndicator.value]: {
color: selColor.value, color: selColor.value,
type: graphType.value, type: graphType.value,
}, };
if (fillTo.value && graphType.value === ChartType.line) {
val.fill_to = fillTo.value;
}
return {
[selAvailableIndicator.value]: val,
}; };
}); });
const emitIndicator = () => {
emit('update:modelValue', combinedIndicator.value);
};
const clickCancel = () => { function emitIndicator() {
cancelled.value = true; emit('update:modelValue', combinedIndicator.value);
emitIndicator(); }
};
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -135,29 +91,26 @@ watch(
[selAvailableIndicator.value] = Object.keys(props.modelValue); [selAvailableIndicator.value] = Object.keys(props.modelValue);
cancelled.value = false; cancelled.value = false;
if (selAvailableIndicator.value && props.modelValue) { if (selAvailableIndicator.value && props.modelValue) {
selColor.value = props.modelValue[selAvailableIndicator.value].color || randomColor(); const xx = props.modelValue[selAvailableIndicator.value];
graphType.value = props.modelValue[selAvailableIndicator.value].type || ChartType.line; selColor.value = xx.color || randomColor();
graphType.value = xx.type || ChartType.line;
fillTo.value = xx.fill_to || '';
} }
}, },
{
immediate: true,
},
); );
watch(selColor, () => { watchDebounced(
if (!props.addNew) { [selColor, graphType, fillTo],
() => {
emitIndicator(); emitIndicator();
} },
}); {
debounce: 200,
},
);
</script> </script>
<style scoped> <style scoped></style>
.colorbox {
border-radius: 50%;
margin-top: auto;
margin-bottom: auto;
height: 25px;
width: 25px;
vertical-align: center;
}
.pointer {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="d-flex flex-row">
<b-form-group class="flex-grow-1" :label="label" label-for="indicatorSelector">
<v-select
v-model="selAvailableIndicator"
:options="columns"
size="sm"
:clearable="false"
@option:selected="emitIndicator"
>
</v-select>
</b-form-group>
<b-button size="sm" title="Abort" class="ms-1 mt-auto" variant="secondary" @click="abort">
<i-mdi-close />
</b-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import vSelect from 'vue-select';
const props = defineProps({
modelValue: { required: false, default: '', type: String },
columns: { required: true, type: Array as () => string[] },
label: { required: true, type: String },
});
const emit = defineEmits(['update:modelValue', 'indicatorSelected']);
const selAvailableIndicator = ref(props.modelValue || '');
function emitIndicator() {
emit('indicatorSelected', selAvailableIndicator.value);
emit('update:modelValue', selAvailableIndicator.value);
}
function abort() {
selAvailableIndicator.value = '';
emitIndicator();
}
watch(
() => props.modelValue,
(newValue) => {
selAvailableIndicator.value = newValue;
},
);
</script>
<style scoped></style>

View File

@ -1,12 +1,13 @@
<template> <template>
<div class="d-flex flex-column h-100 position-relative"> <div class="d-flex flex-column h-100 position-relative">
<div class="flex-grow-1 order-2"> <div class="flex-grow-1 order-2">
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" /> <e-charts v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" />
</div> </div>
<b-form-group <b-form-group
class="w-25 order-1" class="order-1"
:class="showTitle ? 'ms-5 ps-5' : 'position-absolute'" :class="showTitle ? 'ms-5 ps-5' : 'position-absolute'"
label="Bins" label="Bins"
style="width: 33%; min-width: 12rem"
label-for="input-bins" label-for="input-bins"
label-cols="6" label-cols="6"
content-cols="6" content-cols="6"
@ -16,14 +17,15 @@
id="input-bins" id="input-bins"
v-model="settingsStore.profitDistributionBins" v-model="settingsStore.profitDistributionBins"
size="sm" size="sm"
class="mt-1"
:options="binOptions" :options="binOptions"
></b-form-select> ></b-form-select>
</b-form-group> </b-form-group>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import ECharts from 'vue-echarts'; import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
@ -57,16 +59,10 @@ use([
// Define Column labels here to avoid typos // Define Column labels here to avoid typos
const CHART_PROFIT = 'Trade count'; const CHART_PROFIT = 'Trade count';
export default defineComponent({ const props = defineProps({
name: 'ProfitDistributionChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => ClosedTrade[] }, trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean }, showTitle: { default: true, type: Boolean },
}, });
setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
// registerTransform(ecStat.transform.histogram); // registerTransform(ecStat.transform.histogram);
// console.log(profits); // console.log(profits);
@ -140,10 +136,6 @@ export default defineComponent({
}; };
return chartOptionsLoc; return chartOptionsLoc;
}); });
// console.log(chartOptions);
return { settingsStore, chartOptions, binOptions };
},
});
</script> </script>
<style scoped> <style scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<v-chart <e-charts
v-if="trades.length > 0" v-if="trades.length > 0"
:option="chartOptions" :option="chartOptions"
autoresize autoresize
@ -7,7 +7,7 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import ECharts from 'vue-echarts'; import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
@ -26,7 +26,7 @@ import {
import { ClosedTrade } from '@/types'; import { ClosedTrade } from '@/types';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { timestampms } from '@/shared/formatters'; import { timestampms } from '@/shared/formatters';
import { dataZoomPartial } from '@/shared/charts/chartZoom'; import { dataZoomPartial } from '@/shared/charts/chartZoom';
@ -49,16 +49,10 @@ use([
const CHART_PROFIT = 'Profit %'; const CHART_PROFIT = 'Profit %';
const CHART_COLOR = '#9be0a8'; const CHART_COLOR = '#9be0a8';
export default defineComponent({ const props = defineProps({
name: 'TradesLogChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => ClosedTrade[] }, trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean }, showTitle: { default: true, type: Boolean },
}, });
setup(props) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const chartData = computed(() => { const chartData = computed(() => {
const res: (number | string)[][] = []; const res: (number | string)[][] = [];
@ -83,8 +77,7 @@ export default defineComponent({
const chartOptions = computed((): EChartsOption => { const chartOptions = computed((): EChartsOption => {
// const { chartData } = this; // const { chartData } = this;
// Show a maximum of 50 trades by default - allowing to zoom out further. // Show a maximum of 50 trades by default - allowing to zoom out further.
const datazoomStart = const datazoomStart = chartData.value.length > 0 ? (1 - 50 / chartData.value.length) * 100 : 100;
chartData.value.length > 0 ? (1 - 50 / chartData.value.length) * 100 : 100;
return { return {
title: { title: {
text: 'Trades log', text: 'Trades log',
@ -183,10 +176,6 @@ export default defineComponent({
], ],
}; };
}); });
return { settingsStore, chartData, chartOptions };
},
});
</script> </script>
<style scoped> <style scoped>

View File

@ -6,7 +6,7 @@
aria-label="Refresh" aria-label="Refresh"
@click="botStore.activeBot.getBacktestHistory" @click="botStore.activeBot.getBacktestHistory"
> >
&#x21bb; <i-mdi-refresh />
</button> </button>
<p> <p>
Load Historic results from disk. You can click on multiple results to load all of them into Load Historic results from disk. You can click on multiple results to load all of them into
@ -28,25 +28,16 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent, onMounted } from 'vue'; import { onMounted } from 'vue';
import { timestampms } from '@/shared/formatters'; import { timestampms } from '@/shared/formatters';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
onMounted(() => { onMounted(() => {
botStore.activeBot.getBacktestHistory(); botStore.activeBot.getBacktestHistory();
}); });
return {
timestampms,
botStore,
};
},
});
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -37,6 +37,7 @@
showRightBar ? 'col-md-8' : 'col-md-10' showRightBar ? 'col-md-8' : 'col-md-10'
} candle-chart-container px-0 h-100 align-self-stretch`" } candle-chart-container px-0 h-100 align-self-stretch`"
:slider-position="sliderPosition" :slider-position="sliderPosition"
:freqai-model="freqaiModel"
> >
</CandleChartContainer> </CandleChartContainer>
<TradeListNav <TradeListNav
@ -65,6 +66,7 @@ import { ChartSliderPosition, Trade } from '@/types';
defineProps({ defineProps({
timeframe: { required: true, type: String }, timeframe: { required: true, type: String },
strategy: { required: true, type: String }, strategy: { required: true, type: String },
freqaiModel: { required: false, default: undefined, type: String },
timerange: { required: true, type: String }, timerange: { required: true, type: String },
pairlist: { required: true, type: Array as () => string[] }, pairlist: { required: true, type: Array as () => string[] },
trades: { required: true, type: Array as () => Trade[] }, trades: { required: true, type: Array as () => Trade[] },

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { PeriodicBreakdown } from '@/types';
import { TableField, TableItem } from 'bootstrap-vue-next';
import { computed, ref } from 'vue';
defineProps({
periodicBreakdown: {
type: Object as () => PeriodicBreakdown,
required: true,
},
});
const periodicBreakdownSelections = [
{ value: 'day', text: 'Days' },
{ value: 'week', text: 'Weeks' },
{ value: 'month', text: 'Months' },
];
const periodicBreakdownPeriod = ref<string>('day');
const periodicBreakdownFields = computed<TableField[]>(() => {
return [
{ key: 'date', label: 'Date' },
{ key: 'wins', label: 'Wins' },
{ key: 'draws', label: 'Draws' },
{ key: 'loses', label: 'Losses' },
];
});
</script>
<template>
<b-form-radio-group
id="order-direction"
v-model="periodicBreakdownPeriod"
:options="periodicBreakdownSelections"
name="radios-btn-default"
size="sm"
buttons
style="min-width: 10em"
button-variant="outline-primary"
></b-form-radio-group>
<b-table
small
hover
stacked="sm"
:items="periodicBreakdown[periodicBreakdownPeriod] as unknown as TableItem[]"
:fields="periodicBreakdownFields"
>
</b-table>
</template>

View File

@ -16,32 +16,21 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { formatPercent } from '@/shared/formatters'; import { formatPercent } from '@/shared/formatters';
import { StrategyBacktestResult } from '@/types'; import { StrategyBacktestResult } from '@/types';
import { defineComponent } from 'vue'; defineProps({
export default defineComponent({
name: 'BacktestResultSelect',
props: {
backtestHistory: { backtestHistory: {
required: true, required: true,
type: Object as () => Record<string, StrategyBacktestResult>, type: Object as () => Record<string, StrategyBacktestResult>,
}, },
selectedBacktestResultKey: { required: false, default: '', type: String }, selectedBacktestResultKey: { required: false, default: '', type: String },
}, });
emits: ['selectionChange'], const emit = defineEmits(['selectionChange']);
setup(_, { emit }) {
const setBacktestResult = (key) => { const setBacktestResult = (key) => {
emit('selectionChange', key); emit('selectionChange', key);
}; };
return {
formatPercent,
setBacktestResult,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -44,6 +44,15 @@
> >
</b-table> </b-table>
</b-card> </b-card>
<b-card
v-if="backtestResult.periodic_breakdown"
header="Periodic breakdown"
class="row mt-2 w-100"
>
<BacktestResultPeriodBreakdown
:periodic-breakdown="backtestResult.periodic_breakdown"
></BacktestResultPeriodBreakdown>
</b-card>
<b-card header="Single trades" class="row mt-2 w-100"> <b-card header="Single trades" class="row mt-2 w-100">
<TradeList <TradeList
@ -60,6 +69,7 @@
<script setup lang="ts"> <script setup lang="ts">
import TradeList from '@/components/ftbot/TradeList.vue'; import TradeList from '@/components/ftbot/TradeList.vue';
import { StrategyBacktestResult, Trade } from '@/types'; import { StrategyBacktestResult, Trade } from '@/types';
import BacktestResultPeriodBreakdown from './BacktestResultPeriodBreakdown.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { import {

View File

@ -4,20 +4,29 @@
<label class="me-auto h3">Balance</label> <label class="me-auto h3">Balance</label>
<div class="float-end d-flex flex-row"> <div class="float-end d-flex flex-row">
<b-button <b-button
v-if="canUseBotBalance"
size="sm" size="sm"
title="Hide small balances" :title="!showBotOnly ? 'Showing Account balance' : 'Showing Bot balance'"
@click="showBotOnly = !showBotOnly"
>
<i-mdi-robot v-if="showBotOnly" />
<i-mdi-bank v-else />
</b-button>
<b-button
size="sm"
:title="!hideSmallBalances ? 'Hide small balances' : 'Show all balances'"
@click="hideSmallBalances = !hideSmallBalances" @click="hideSmallBalances = !hideSmallBalances"
> >
<HideIcon v-if="hideSmallBalances" :size="16" /> <i-mdi-eye-off v-if="hideSmallBalances" />
<ShowIcon v-else :size="16" /> <i-mdi-eye v-else />
</b-button> </b-button>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance" <b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance">
>&#x21bb;</b-button <i-mdi-refresh />
> </b-button>
</div> </div>
</div> </div>
<BalanceChart v-if="balanceCurrencies" :currencies="balanceCurrencies" /> <BalanceChart v-if="balanceCurrencies" :currencies="chartValues" />
<div> <div>
<p v-if="botStore.activeBot.balance.note"> <p v-if="botStore.activeBot.balance.note">
<strong>{{ botStore.activeBot.balance.note }}</strong> <strong>{{ botStore.activeBot.balance.note }}</strong>
@ -36,7 +45,11 @@
</td> </td>
<!-- this is a computed prop that adds up all the expenses in the visible rows --> <!-- this is a computed prop that adds up all the expenses in the visible rows -->
<td> <td>
<strong>{{ formatCurrency(botStore.activeBot.balance.total) }}</strong> <strong>{{
showBotOnly && canUseBotBalance
? formatCurrency(botStore.activeBot.balance.total_bot)
: formatCurrency(botStore.activeBot.balance.total)
}}</strong>
</td> </td>
</template> </template>
</b-table> </b-table>
@ -45,38 +58,64 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import HideIcon from 'vue-material-design-icons/EyeOff.vue';
import ShowIcon from 'vue-material-design-icons/Eye.vue';
import BalanceChart from '@/components/charts/BalanceChart.vue'; import BalanceChart from '@/components/charts/BalanceChart.vue';
import { formatPercent, formatPrice } from '@/shared/formatters'; import { formatPercent, formatPrice } from '@/shared/formatters';
import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { BalanceValues } from '@/types';
import { TableField } from 'bootstrap-vue-next';
import { computed, ref } from 'vue';
const botStore = useBotStore(); const botStore = useBotStore();
const hideSmallBalances = ref(true); const hideSmallBalances = ref(true);
const showBotOnly = ref(true);
const smallBalance = computed((): number => { const smallBalance = computed<number>(() => {
return Number((0.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8)); return Number((1.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8));
});
const canUseBotBalance = computed(() => {
return botStore.activeBot.botApiVersion >= 2.26;
}); });
const balanceCurrencies = computed(() => { const balanceCurrencies = computed(() => {
if (!hideSmallBalances.value) { return botStore.activeBot.balance.currencies?.filter(
return botStore.activeBot.balance.currencies; (v) =>
} (!hideSmallBalances.value || v.est_stake >= smallBalance.value) &&
(!canUseBotBalance.value || !showBotOnly.value || (v.is_bot_managed ?? true) === true),
return botStore.activeBot.balance.currencies?.filter((v) => v.est_stake >= smallBalance.value); );
}); });
const formatCurrency = (value) => { const formatCurrency = (value) => {
return value ? formatPrice(value, 5) : ''; return value ? formatPrice(value, botStore.activeBot.stakeCurrencyDecimals) : '';
}; };
const tableFields = computed(() => { const chartValues = computed<BalanceValues[]>(() => {
return balanceCurrencies.value?.map((v) => {
return {
balance:
showBotOnly.value && canUseBotBalance.value && v.bot_owned != undefined
? v.bot_owned
: v.balance,
currency: v.currency,
est_stake:
showBotOnly.value && canUseBotBalance.value ? v.est_stake_bot ?? v.est_stake : v.est_stake,
free: showBotOnly.value && canUseBotBalance.value ? v.bot_owned ?? v.free : v.free,
used: v.used,
stake: v.stake,
};
});
});
const tableFields = computed<TableField[]>(() => {
return [ return [
{ key: 'currency', label: 'Currency' }, { key: 'currency', label: 'Currency' },
{ key: 'free', label: 'Available', formatter: formatCurrency },
{ {
key: 'est_stake', key: showBotOnly.value && canUseBotBalance.value ? 'bot_owned' : 'free',
label: 'Available',
formatter: formatCurrency,
},
{
key: showBotOnly.value && canUseBotBalance.value ? 'est_stake_bot' : 'est_stake',
label: `in ${botStore.activeBot.balance.stake}`, label: `in ${botStore.activeBot.balance.stake}`,
formatter: formatCurrency, formatter: formatCurrency,
}, },

View File

@ -52,18 +52,14 @@
</b-table> </b-table>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import ProfitPill from '@/components/general/ProfitPill.vue'; import ProfitPill from '@/components/general/ProfitPill.vue';
import { formatPrice } from '@/shared/formatters'; import { formatPrice } from '@/shared/formatters';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { ProfitInterface, ComparisonTableItems } from '@/types'; import { ProfitInterface, ComparisonTableItems } from '@/types';
import { TableField, TableItem } from 'bootstrap-vue-next'; import { TableField, TableItem } from 'bootstrap-vue-next';
export default defineComponent({
name: 'BotComparisonList',
components: { ProfitPill },
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
const tableFields: TableField[] = [ const tableFields: TableField[] = [
@ -110,7 +106,7 @@ export default defineComponent({
profitOpen, profitOpen,
wins: v.winning_trades, wins: v.winning_trades,
losses: v.losing_trades, losses: v.losing_trades,
balance: botStore.allBalance[k]?.total, balance: botStore.allBalance[k]?.total_bot ?? botStore.allBalance[k]?.total,
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3, stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
}); });
if (v.profit_closed_coin !== undefined) { if (v.profit_closed_coin !== undefined) {
@ -124,15 +120,6 @@ export default defineComponent({
val.push(summary); val.push(summary);
return val as unknown as TableItem[]; return val as unknown as TableItem[];
}); });
return {
formatPrice,
tableFields,
tableItems,
botStore,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -7,7 +7,7 @@ forceexit
title="Start Trading" title="Start Trading"
@click="botStore.activeBot.startBot()" @click="botStore.activeBot.startBot()"
> >
<PlayIcon /> <i-mdi-play height="24" width="24" />
</button> </button>
<button <button
class="btn btn-secondary btn-sm ms-1" class="btn btn-secondary btn-sm ms-1"
@ -15,7 +15,7 @@ forceexit
title="Stop Trading - Also stops handling open trades." title="Stop Trading - Also stops handling open trades."
@click="handleStopBot()" @click="handleStopBot()"
> >
<StopIcon /> <i-mdi-stop height="24" width="24" />
</button> </button>
<button <button
class="btn btn-secondary btn-sm ms-1" class="btn btn-secondary btn-sm ms-1"
@ -23,7 +23,7 @@ forceexit
title="StopBuy - Stops buying, but still handles open trades" title="StopBuy - Stops buying, but still handles open trades"
@click="handleStopBuy()" @click="handleStopBuy()"
> >
<PauseIcon /> <i-mdi-pause height="24" width="24" />
</button> </button>
<button <button
class="btn btn-secondary btn-sm ms-1" class="btn btn-secondary btn-sm ms-1"
@ -31,7 +31,7 @@ forceexit
title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly." title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly."
@click="handleReloadConfig()" @click="handleReloadConfig()"
> >
<ReloadIcon /> <i-mdi-reload height="24" width="24" />
</button> </button>
<button <button
class="btn btn-secondary btn-sm ms-1" class="btn btn-secondary btn-sm ms-1"
@ -39,20 +39,16 @@ forceexit
title="Force exit all" title="Force exit all"
@click="handleForceExit()" @click="handleForceExit()"
> >
<ForceExitIcon /> <i-mdi-close-box-multiple height="24" width="24" />
</button> </button>
<button <button
v-if=" v-if="botStore.activeBot.botState && botStore.activeBot.botState.force_entry_enable"
botStore.activeBot.botState &&
(botStore.activeBot.botState.force_entry_enable ||
botStore.activeBot.botState.forcebuy_enabled)
"
class="btn btn-secondary btn-sm ms-1" class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading || !isRunning" :disabled="!botStore.activeBot.isTrading || !isRunning"
title="Force enter - Immediately enter a trade at an optional price. Exits are then handled according to strategy rules." title="Force enter - Immediately enter a trade at an optional price. Exits are then handled according to strategy rules."
@click="forceEnter = true" @click="forceEnter = true"
> >
<ForceEntryIcon /> <i-mdi-plus-box-multiple-outline style="font-size: 20px" />
</button> </button>
<button <button
v-if="botStore.activeBot.isWebserverMode && false" v-if="botStore.activeBot.isWebserverMode && false"
@ -61,39 +57,21 @@ forceexit
title="Start Trading mode" title="Start Trading mode"
@click="botStore.activeBot.startTrade()" @click="botStore.activeBot.startTrade()"
> >
<PlayIcon /> <i-mdi-play class="fs-4" />
</button> </button>
<ForceEntryForm v-model="forceEnter" :pair="botStore.activeBot.selectedPair" /> <ForceEntryForm v-model="forceEnter" :pair="botStore.activeBot.selectedPair" />
<MessageBox ref="msgBox" /> <MessageBox ref="msgBox" />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { ForceSellPayload } from '@/types';
import PlayIcon from 'vue-material-design-icons/Play.vue';
import StopIcon from 'vue-material-design-icons/Stop.vue';
import PauseIcon from 'vue-material-design-icons/Pause.vue';
import ReloadIcon from 'vue-material-design-icons/Reload.vue';
import ForceExitIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
import ForceEntryIcon from 'vue-material-design-icons/PlusBoxMultipleOutline.vue';
import ForceEntryForm from './ForceEntryForm.vue';
import MessageBox, { MsgBoxObject } from '@/components/general/MessageBox.vue'; import MessageBox, { MsgBoxObject } from '@/components/general/MessageBox.vue';
import { defineComponent, computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { ForceSellPayload } from '@/types';
import { computed, ref } from 'vue';
import ForceEntryForm from './ForceEntryForm.vue';
export default defineComponent({
name: 'BotControls',
components: {
ForceEntryForm,
PlayIcon,
StopIcon,
PauseIcon,
ReloadIcon,
ForceExitIcon,
ForceEntryIcon,
MessageBox,
},
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
const forceEnter = ref<boolean>(false); const forceEnter = ref<boolean>(false);
const msgBox = ref<typeof MessageBox>(); const msgBox = ref<typeof MessageBox>();
@ -150,16 +128,4 @@ export default defineComponent({
}; };
msgBox.value?.show(msg); msgBox.value?.show(msg);
}; };
return {
handleStopBot,
handleStopBuy,
handleReloadConfig,
handleForceExit,
forceEnter,
botStore,
isRunning,
msgBox,
};
},
});
</script> </script>

View File

@ -11,15 +11,12 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { formatPrice } from '@/shared/formatters'; import { formatPrice } from '@/shared/formatters';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { TableField } from 'bootstrap-vue-next'; import { TableField } from 'bootstrap-vue-next';
export default defineComponent({
name: 'BotPerformance',
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
const tableFields = computed<TableField[]>(() => { const tableFields = computed<TableField[]>(() => {
return [ return [
@ -33,10 +30,4 @@ export default defineComponent({
{ key: 'count', label: 'Count' }, { key: 'count', label: 'Count' },
]; ];
}); });
return {
tableFields,
botStore,
};
},
});
</script> </script>

View File

@ -27,13 +27,7 @@
<p> <p>
Currently <strong>{{ botStore.activeBot.botState.state }}</strong Currently <strong>{{ botStore.activeBot.botState.state }}</strong
>, >,
<strong <strong>force entry: {{ botStore.activeBot.botState.force_entry_enable }}</strong>
>force entry:
{{
botStore.activeBot.botState.force_entry_enable ||
botStore.activeBot.botState.forcebuy_enabled
}}</strong
>
</p> </p>
<p> <p>
<strong>{{ botStore.activeBot.botState.dry_run ? 'Dry-Run' : 'Live' }}</strong> <strong>{{ botStore.activeBot.botState.dry_run ? 'Dry-Run' : 'Live' }}</strong>
@ -85,23 +79,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { formatPercent, formatPriceCurrency } from '@/shared/formatters'; import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
import DateTimeTZ from '@/components/general/DateTimeTZ.vue'; import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'BotStatus',
components: { DateTimeTZ },
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
return {
formatPercent,
formatPriceCurrency,
botStore,
};
},
});
</script> </script>

View File

@ -33,19 +33,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { formatPrice } from '@/shared/formatters';
import { Trade } from '@/types'; import { Trade } from '@/types';
import CustomTradeListEntry from '@/components/ftbot/CustomTradeListEntry.vue'; import CustomTradeListEntry from '@/components/ftbot/CustomTradeListEntry.vue';
import { defineComponent, computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({ const props = defineProps({
name: 'CustomTradeList',
components: {
CustomTradeListEntry,
},
props: {
trades: { required: true, type: Array as () => Trade[] }, trades: { required: true, type: Array as () => Trade[] },
title: { default: 'Trades', type: String }, title: { default: 'Trades', type: String },
stakeCurrency: { required: false, default: '', type: String }, stakeCurrency: { required: false, default: '', type: String },
@ -54,8 +48,7 @@ export default defineComponent({
multiBotView: { default: false, type: Boolean }, multiBotView: { default: false, type: Boolean },
emptyText: { default: 'No Trades to show.', type: String }, emptyText: { default: 'No Trades to show.', type: String },
stakeCurrencyDecimals: { default: 3, type: Number }, stakeCurrencyDecimals: { default: 3, type: Number },
}, });
setup(props) {
const botStore = useBotStore(); const botStore = useBotStore();
const currentPage = ref(1); const currentPage = ref(1);
const filterText = ref(''); const filterText = ref('');
@ -66,37 +59,10 @@ export default defineComponent({
const filteredTrades = computed(() => { const filteredTrades = computed(() => {
return props.trades.slice((currentPage.value - 1) * perPage, currentPage.value * perPage); return props.trades.slice((currentPage.value - 1) * perPage, currentPage.value * perPage);
}); });
const formatPriceWithDecimals = (price) => {
return formatPrice(price, props.stakeCurrencyDecimals);
};
const handleContextMenuEvent = (item, index, event) => {
// stop browser context menu from appearing
if (!props.activeTrades) {
return;
}
event.preventDefault();
// log the selected item to the console
console.log(item);
};
const tradeClick = (trade) => { const tradeClick = (trade) => {
botStore.activeBot.setDetailTrade(trade); botStore.activeBot.setDetailTrade(trade);
}; };
return {
currentPage,
filterText,
perPage,
filteredTrades,
formatPriceWithDecimals,
handleContextMenuEvent,
tradeClick,
botStore,
rows,
};
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -15,21 +15,12 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { formatPercent, formatPrice } from '@/shared/formatters';
import { Trade } from '@/types'; import { Trade } from '@/types';
import DateTimeTZ from '@/components/general/DateTimeTZ.vue'; import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
import TradeProfit from './TradeProfit.vue'; import TradeProfit from './TradeProfit.vue';
export default defineComponent({ defineProps({
components: {
DateTimeTZ,
TradeProfit,
},
props: {
trade: { trade: {
type: Object as () => Trade, type: Object as () => Trade,
required: true, required: true,
@ -42,13 +33,6 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
},
setup() {
return {
formatPrice,
formatPercent,
};
},
}); });
</script> </script>

View File

@ -2,7 +2,9 @@
<div> <div>
<div class="mb-2"> <div class="mb-2">
<label class="me-auto h3">Daily Stats</label> <label class="me-auto h3">Daily Stats</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getDaily">&#x21bb;</b-button> <b-button class="float-end" size="sm" @click="botStore.activeBot.getDaily">
<i-mdi-refresh />
</b-button>
</div> </div>
<div> <div>
<DailyChart <DailyChart
@ -17,19 +19,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import DailyChart from '@/components/charts/DailyChart.vue'; import DailyChart from '@/components/charts/DailyChart.vue';
import { formatPercent } from '@/shared/formatters'; import { formatPercent } from '@/shared/formatters';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { TableField } from 'bootstrap-vue-next'; import { TableField } from 'bootstrap-vue-next';
export default defineComponent({
name: 'DailyStats',
components: {
DailyChart,
},
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
const dailyFields = computed<TableField[]>(() => { const dailyFields = computed<TableField[]>(() => {
const res: TableField[] = [ const res: TableField[] = [
@ -57,11 +53,4 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
botStore.activeBot.getDaily(); botStore.activeBot.getDaily();
}); });
return {
botStore,
dailyFields,
};
},
});
</script> </script>

View File

@ -33,7 +33,7 @@
class="me-1" class="me-1"
:class="botStore.activeBot.botApiVersion >= 1.12 ? 'col-6' : ''" :class="botStore.activeBot.botApiVersion >= 1.12 ? 'col-6' : ''"
size="sm" size="sm"
>+ ><i-mdi-plus-box-outline />
</b-button> </b-button>
<b-button <b-button
v-if="botStore.activeBot.botApiVersion >= 1.12" v-if="botStore.activeBot.botApiVersion >= 1.12"
@ -43,7 +43,7 @@
:disabled="blacklistSelect.length === 0" :disabled="blacklistSelect.length === 0"
@click="deletePairs" @click="deletePairs"
> >
<DeleteIcon :size="16" title="Delete Bot" /> <i-mdi-delete />
</b-button> </b-button>
</div> </div>
<b-popover <b-popover
@ -81,7 +81,7 @@
class="pair black" class="pair black"
:active="blacklistSelect.indexOf(key) > -1" :active="blacklistSelect.indexOf(key) > -1"
@click="blacklistSelectClick(key)" @click="blacklistSelectClick(key)"
><span class="check">&#x2714;</span>{{ pair }}</b-list-group-item ><span class="check"><i-mdi-check-circle /></span>{{ pair }}</b-list-group-item
> >
</b-list-group> </b-list-group>
</div> </div>
@ -91,15 +91,10 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import { defineComponent, ref, onMounted } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { onMounted, ref } from 'vue';
export default defineComponent({
name: 'FTBotAPIPairList',
components: { DeleteIcon },
setup() {
const newblacklistpair = ref(''); const newblacklistpair = ref('');
const blackListShow = ref(false); const blackListShow = ref(false);
const blacklistSelect = ref<number[]>([]); const blacklistSelect = ref<number[]>([]);
@ -149,31 +144,20 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
initBlacklist(); initBlacklist();
}); });
return {
addBlacklistPair,
deletePairs,
initBlacklist,
blacklistSelectClick,
botStore,
newblacklistpair,
blackListShow,
blacklistSelect,
};
},
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.check { .check {
// Hidden checkbox on blacklist selection // Hidden checkbox on blacklist selection
background: #41b883; // background: white;
color: #41b883;
opacity: 0; opacity: 0;
border-radius: 50%; // border-radius: 50%;
z-index: 5; z-index: 5;
width: 1.3em; width: 1.3em;
height: 1.3em; height: 1.3em;
top: -0.2em; top: -0.3em;
left: -0.2em; left: -0.3em;
position: absolute; position: absolute;
transition: opacity 0.2s; transition: opacity 0.2s;
} }

View File

@ -56,7 +56,6 @@
></b-form-input> ></b-form-input>
</b-form-group> </b-form-group>
<b-form-group <b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*Stake-amount in ${botStore.activeBot.stakeCurrency} [optional]`" :label="`*Stake-amount in ${botStore.activeBot.stakeCurrency} [optional]`"
label-for="stake-input" label-for="stake-input"
invalid-feedback="Stake-amount must be empty or a positive number" invalid-feedback="Stake-amount must be empty or a positive number"
@ -86,7 +85,6 @@
></b-form-input> ></b-form-input>
</b-form-group> </b-form-group>
<b-form-group <b-form-group
v-if="botStore.activeBot.botApiVersion > 1.1"
label="OrderType" label="OrderType"
label-for="ordertype-input" label-for="ordertype-input"
invalid-feedback="OrderType" invalid-feedback="OrderType"
@ -202,14 +200,12 @@ const resetForm = () => {
selectedPair.value = props.pair; selectedPair.value = props.pair;
price.value = undefined; price.value = undefined;
stakeAmount.value = undefined; stakeAmount.value = undefined;
if (botStore.activeBot.botApiVersion > 1.1) {
ordertype.value = ordertype.value =
botStore.activeBot.botState?.order_types?.forcebuy || botStore.activeBot.botState?.order_types?.forcebuy ||
botStore.activeBot.botState?.order_types?.force_entry || botStore.activeBot.botState?.order_types?.force_entry ||
botStore.activeBot.botState?.order_types?.buy || botStore.activeBot.botState?.order_types?.buy ||
botStore.activeBot.botState?.order_types?.entry || botStore.activeBot.botState?.order_types?.entry ||
'limit'; 'limit';
}
}; };
const handleEntry = () => { const handleEntry = () => {

View File

@ -15,7 +15,6 @@
<span>Currently owning {{ trade.amount }} {{ trade.base_currency }}</span> <span>Currently owning {{ trade.amount }} {{ trade.base_currency }}</span>
</p> </p>
<b-form-group <b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*Amount in ${trade.base_currency} [optional]`" :label="`*Amount in ${trade.base_currency} [optional]`"
label-for="stake-input" label-for="stake-input"
invalid-feedback="Amount must be empty or a positive number" invalid-feedback="Amount must be empty or a positive number"
@ -40,7 +39,6 @@
</b-form-group> </b-form-group>
<b-form-group <b-form-group
v-if="botStore.activeBot.botApiVersion > 1.1"
label="*OrderType" label="*OrderType"
label-for="ordertype-input" label-for="ordertype-input"
invalid-feedback="OrderType" invalid-feedback="OrderType"
@ -114,12 +112,10 @@ const handleSubmit = () => {
}; };
const resetForm = () => { const resetForm = () => {
amount.value = props.trade.amount; amount.value = props.trade.amount;
if (botStore.activeBot.botApiVersion > 1.1) {
ordertype.value = ordertype.value =
botStore.activeBot.botState?.order_types?.force_exit || botStore.activeBot.botState?.order_types?.force_exit ||
botStore.activeBot.botState?.order_types?.exit || botStore.activeBot.botState?.order_types?.exit ||
'limit'; 'limit';
}
}; };
const handleEntry = () => { const handleEntry = () => {

View File

@ -8,7 +8,9 @@
> >
</b-form-select> </b-form-select>
<div class="ms-2"> <div class="ms-2">
<b-button @click="botStore.activeBot.getFreqAIModelList">&#x21bb;</b-button> <b-button @click="botStore.activeBot.getFreqAIModelList">
<i-mdi-refresh />
</b-button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,16 @@
<template> <template>
<div class="d-flex h-100 p-0 align-items-start"> <div class="d-flex h-100 p-0 align-items-start">
<textarea v-model="formattedLogs" class="h-100" readonly></textarea> <textarea v-model="formattedLogs" class="h-100" readonly></textarea>
<b-button id="refresh-logs" size="sm" @click="botStore.activeBot.getLogs">&#x21bb;</b-button> <b-button id="refresh-logs" size="sm" @click="botStore.activeBot.getLogs">
<i-mdi-refresh />
</b-button>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { defineComponent, onMounted, computed } from 'vue'; import { onMounted, computed } from 'vue';
export default defineComponent({
name: 'LogViewer',
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
onMounted(async () => { onMounted(async () => {
@ -26,13 +25,6 @@ export default defineComponent({
} }
return result; return result;
}); });
return {
botStore,
formattedLogs,
};
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -2,7 +2,9 @@
<div> <div>
<div class="mb-2"> <div class="mb-2">
<label class="me-auto h3">Pair Locks</label> <label class="me-auto h3">Pair Locks</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getLocks">&#x21bb;</b-button> <b-button class="float-end" size="sm" @click="botStore.activeBot.getLocks">
<i-mdi-refresh />
</b-button>
</div> </div>
<div> <div>
<b-table class="table-sm" :items="botStore.activeBot.activeLocks" :fields="tableFields"> <b-table class="table-sm" :items="botStore.activeBot.activeLocks" :fields="tableFields">
@ -13,7 +15,7 @@
title="Delete trade" title="Delete trade"
@click="removePairLock(row.item)" @click="removePairLock(row.item)"
> >
<DeleteIcon :size="16" /> <i-mdi-delete />
</b-button> </b-button>
</template> </template>
</b-table> </b-table>
@ -21,24 +23,18 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { timestampms } from '@/shared/formatters'; import { timestampms } from '@/shared/formatters';
import { Lock } from '@/types'; import { Lock } from '@/types';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import { showAlert } from '@/stores/alerts'; import { showAlert } from '@/stores/alerts';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
import { TableField } from 'bootstrap-vue-next';
export default defineComponent({
name: 'PairLockList',
components: { DeleteIcon },
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
const tableFields = [ const tableFields: TableField[] = [
{ key: 'pair', label: 'Pair' }, { key: 'pair', label: 'Pair' },
{ key: 'lock_end_timestamp', label: 'Until', formatter: 'timestampms' }, { key: 'lock_end_timestamp', label: 'Until', formatter: (value) => timestampms(value as number) },
{ key: 'reason', label: 'Reason' }, { key: 'reason', label: 'Reason' },
{ key: 'actions' }, { key: 'actions' },
]; ];
@ -51,15 +47,6 @@ export default defineComponent({
showAlert('This Freqtrade version does not support deleting locks.'); showAlert('This Freqtrade version does not support deleting locks.');
} }
}; };
return {
timestampms,
botStore,
tableFields,
removePairLock,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -11,7 +11,7 @@
> >
<div> <div>
{{ comb.pair }} {{ comb.pair }}
<span v-if="comb.locks" :title="comb.lockReason"> &#128274; </span> <span v-if="comb.locks" :title="comb.lockReason"> <i-mdi-lock /> </span>
</div> </div>
<TradeProfit v-if="comb.trade && !backtestMode" :trade="comb.trade" /> <TradeProfit v-if="comb.trade && !backtestMode" :trade="comb.trade" />

View File

@ -12,20 +12,15 @@
title="Auto Refresh All bots" title="Auto Refresh All bots"
@click="botStore.allRefreshFull" @click="botStore.allRefreshFull"
> >
<RefreshIcon :size="16" /> <i-mdi-refresh />
</b-button> </b-button>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import RefreshIcon from 'vue-material-design-icons/Refresh.vue'; import { computed } from 'vue';
import { defineComponent, computed } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'ReloadControl',
components: { RefreshIcon },
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
const autoRefreshLoc = computed({ const autoRefreshLoc = computed({
get() { get() {
@ -35,13 +30,6 @@ export default defineComponent({
botStore.setGlobalAutoRefresh(newValue); botStore.setGlobalAutoRefresh(newValue);
}, },
}); });
return {
botStore,
autoRefreshLoc,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -8,7 +8,9 @@
> >
</b-form-select> </b-form-select>
<div class="ms-2"> <div class="ms-2">
<b-button @click="botStore.activeBot.getStrategyList">&#x21bb;</b-button> <b-button @click="botStore.activeBot.getStrategyList">
<i-mdi-refresh />
</b-button>
</div> </div>
</div> </div>

View File

@ -7,17 +7,14 @@
></b-form-select> ></b-form-select>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, computed, ref } from 'vue'; import { computed, ref } from 'vue';
export default defineComponent({ const props = defineProps({
name: 'TimefameSelect',
props: {
value: { default: '', type: String }, value: { default: '', type: String },
belowTimeframe: { required: false, default: '', type: String }, belowTimeframe: { required: false, default: '', type: String },
}, });
emits: ['input'], const emit = defineEmits(['input']);
setup(props, { emit }) {
const selectedTimeframe = ref(''); const selectedTimeframe = ref('');
// The below list must always remain sorted correctly! // The below list must always remain sorted correctly!
const availableTimeframesBase = [ const availableTimeframesBase = [
@ -54,15 +51,6 @@ export default defineComponent({
const emitSelectedTimeframe = () => { const emitSelectedTimeframe = () => {
emit('input', selectedTimeframe.value); emit('input', selectedTimeframe.value);
}; };
return {
availableTimeframesBase,
availableTimeframes,
emitSelectedTimeframe,
selectedTimeframe,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -7,7 +7,7 @@
title="Forceexit" title="Forceexit"
@click="$emit('forceExit', trade)" @click="$emit('forceExit', trade)"
> >
<ForceSellIcon :size="16" title="Forceexit" class="me-1" />Forceexit <i-mdi-close-box class="me-1" />Forceexit
</b-button> </b-button>
<b-button <b-button
v-if="botApiVersion > 1.1" v-if="botApiVersion > 1.1"
@ -16,7 +16,7 @@
title="Forceexit limit" title="Forceexit limit"
@click="$emit('forceExit', trade, 'limit')" @click="$emit('forceExit', trade, 'limit')"
> >
<ForceSellIcon :size="16" title="Forceexit limit" class="me-1" />Forceexit limit <i-mdi-close-box class="me-1" />Forceexit limit
</b-button> </b-button>
<b-button <b-button
v-if="botApiVersion > 1.1" v-if="botApiVersion > 1.1"
@ -25,7 +25,7 @@
title="Forceexit market" title="Forceexit market"
@click="$emit('forceExit', trade, 'market')" @click="$emit('forceExit', trade, 'market')"
> >
<ForceSellIcon :size="16" title="Forceexit market" class="me-1" />Forceexit market <i-mdi-close-box class="me-1" />Forceexit market
</b-button> </b-button>
<b-button <b-button
v-if="botApiVersion > 2.16" v-if="botApiVersion > 2.16"
@ -34,7 +34,7 @@
title="Forceexit partial" title="Forceexit partial"
@click="$emit('forceExitPartial', trade)" @click="$emit('forceExitPartial', trade)"
> >
<ForceSellPartialIcon :size="16" title="Forceexit partial" class="me-1" />Forceexit partial <i-mdi-close-box-multiple class="me-1" />Forceexit partial
</b-button> </b-button>
<b-button <b-button
v-if="botApiVersion >= 2.24 && trade.open_order_id" v-if="botApiVersion >= 2.24 && trade.open_order_id"
@ -43,16 +43,24 @@
title="Cancel open orders" title="Cancel open orders"
@click="$emit('cancelOpenOrder', trade)" @click="$emit('cancelOpenOrder', trade)"
> >
<CancelIcon :size="16" title="Cancel open order" class="me-1" />Cancel open order <i-mdi-cancel class="me-1" />Cancel open order
</b-button>
<b-button
v-if="botApiVersion >= 2.28"
class="btn-xs text-start mt-1"
size="sm"
title="Reload"
@click="$emit('reloadTrade', trade)"
>
<i-mdi-reload-alert class="me-1" />Reload Trade
</b-button> </b-button>
<b-button <b-button
class="btn-xs text-start mt-1" class="btn-xs text-start mt-1"
size="sm" size="sm"
title="Delete trade" title="Delete trade"
@click="$emit('deleteTrade', trade)" @click="$emit('deleteTrade', trade)"
> >
<DeleteIcon :size="16" title="Delete trade" class="me-1" /> <i-mdi-delete class="me-1" />
Delete Delete
</b-button> </b-button>
</div> </div>
@ -60,10 +68,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Trade } from '@/types'; import { Trade } from '@/types';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import ForceSellPartialIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
import ForceSellIcon from 'vue-material-design-icons/CloseBox.vue';
import CancelIcon from 'vue-material-design-icons/Cancel.vue';
defineProps({ defineProps({
botApiVersion: { botApiVersion: {
@ -75,7 +79,7 @@ defineProps({
required: true, required: true,
}, },
}); });
defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'deleteTrade']); defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'reloadTrade', 'deleteTrade']);
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -1,30 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import ActionIcon from 'vue-material-design-icons/GestureTap.vue';
import TradeActions from './TradeActions.vue';
import CancelIcon from 'vue-material-design-icons/Cancel.vue';
import { Trade } from '@/types'; import { Trade } from '@/types';
import { ref } from 'vue'; import { ref } from 'vue';
import TradeActions from './TradeActions.vue';
defineProps({ defineProps({
trade: { type: Object as () => Trade, required: true }, trade: { type: Object as () => Trade, required: true },
id: { type: Number, required: true }, id: { type: Number, required: true },
botApiVersion: { type: Number, required: true }, botApiVersion: { type: Number, required: true },
}); });
const emit = defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'deleteTrade']); const emit = defineEmits([
'forceExit',
'forceExitPartial',
'cancelOpenOrder',
'reloadTrade',
'deleteTrade',
]);
const popoverOpen = ref(false); const popoverOpen = ref(false);
const forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => { function forceExitHandler(item: Trade, ordertype: string | undefined = undefined) {
popoverOpen.value = false; popoverOpen.value = false;
emit('forceExit', item, ordertype); emit('forceExit', item, ordertype);
}; }
const forceExitPartialHandler = (item: Trade) => { function forceExitPartialHandler(item: Trade) {
popoverOpen.value = false; popoverOpen.value = false;
emit('forceExitPartial', item); emit('forceExitPartial', item);
}; }
const cancelOpenOrderHandler = (item: Trade) => { function cancelOpenOrderHandler(item: Trade) {
popoverOpen.value = false; popoverOpen.value = false;
emit('cancelOpenOrder', item); emit('cancelOpenOrder', item);
}; }
function handleReloadTrade(item: Trade) {
popoverOpen.value = false;
emit('reloadTrade', item);
}
</script> </script>
<template> <template>
@ -36,7 +44,7 @@ const cancelOpenOrderHandler = (item: Trade) => {
title="Actions" title="Actions"
@click="popoverOpen = !popoverOpen" @click="popoverOpen = !popoverOpen"
> >
<ActionIcon :size="16" title="Actions" /> <i-mdi-gesture-tap />
</b-button> </b-button>
<b-popover <b-popover
:target="`btn-actions-${id}`" :target="`btn-actions-${id}`"
@ -55,9 +63,10 @@ const cancelOpenOrderHandler = (item: Trade) => {
$emit('deleteTrade', trade); $emit('deleteTrade', trade);
" "
@cancel-open-order="cancelOpenOrderHandler" @cancel-open-order="cancelOpenOrderHandler"
@reload-trade="handleReloadTrade"
/> />
<b-button class="mt-1 w-100 text-start" size="sm" @click="popoverOpen = false"> <b-button class="mt-1 w-100 text-start" size="sm" @click="popoverOpen = false">
<CancelIcon :size="16" title="Close popup" class="me-1" />Close Actions menu <i-mdi-cancel class="me-1" />Close Actions menu
</b-button> </b-button>
</b-popover> </b-popover>
</div> </div>

View File

@ -128,25 +128,16 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { formatPercent, formatPriceCurrency, formatPrice, timestampms } from '@/shared/formatters'; import { formatPercent, formatPriceCurrency, formatPrice, timestampms } from '@/shared/formatters';
import ValuePair from '@/components/general/ValuePair.vue'; import ValuePair from '@/components/general/ValuePair.vue';
import TradeProfit from '@/components/ftbot/TradeProfit.vue'; import TradeProfit from '@/components/ftbot/TradeProfit.vue';
import DateTimeTZ from '@/components/general/DateTimeTZ.vue'; import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
import { Trade } from '@/types'; import { Trade } from '@/types';
import { defineComponent } from 'vue'; defineProps({
export default defineComponent({
name: 'TradeDetail',
components: { ValuePair, TradeProfit, DateTimeTZ },
props: {
trade: { required: true, type: Object as () => Trade }, trade: { required: true, type: Object as () => Trade },
stakeCurrency: { required: true, type: String }, stakeCurrency: { required: true, type: String },
},
setup() {
return { timestampms, formatPercent, formatPrice, formatPriceCurrency };
},
}); });
</script> </script>
<style scoped> <style scoped>

View File

@ -35,6 +35,7 @@
@force-exit="forceExitHandler" @force-exit="forceExitHandler"
@force-exit-partial="forceExitPartialHandler" @force-exit-partial="forceExitPartialHandler"
@cancel-open-order="cancelOpenOrderHandler" @cancel-open-order="cancelOpenOrderHandler"
@reload-trade="reloadTradeHandler"
/> />
</template> </template>
<template #cell(pair)="row"> <template #cell(pair)="row">
@ -76,14 +77,9 @@
:per-page="perPage" :per-page="perPage"
aria-controls="my-table" aria-controls="my-table"
></b-pagination> ></b-pagination>
<b-form-input <b-form-group v-if="showFilter" label-for="trade-filter">
v-if="showFilter" <b-form-input id="trade-filter" v-model="filterText" type="text" placeholder="Filter" />
v-model="filterText" </b-form-group>
type="text"
placeholder="Filter"
size="sm"
style="width: unset"
/>
</div> </div>
<force-exit-form v-if="activeTrades" v-model="forceExitVisible" :trade="feTrade" /> <force-exit-form v-if="activeTrades" v-model="forceExitVisible" :trade="feTrade" />
<b-modal v-model="removeTradeVisible" title="Exit trade" @ok="forceExitExecuter"> <b-modal v-model="removeTradeVisible" title="Exit trade" @ok="forceExitExecuter">
@ -246,6 +242,10 @@ const cancelOpenOrderHandler = (item: Trade) => {
removeTradeVisible.value = true; removeTradeVisible.value = true;
}; };
function reloadTradeHandler(item: Trade) {
botStore.reloadTradeMulti({ tradeid: String(item.trade_id), botId: item.botId });
}
const handleContextMenuEvent = (item, index, event) => { const handleContextMenuEvent = (item, index, event) => {
// stop browser context menu from appearing // stop browser context menu from appearing
if (!props.activeTrades) { if (!props.activeTrades) {

View File

@ -2,19 +2,16 @@
<span :title="timezoneTooltip">{{ formattedDate }}</span> <span :title="timezoneTooltip">{{ formattedDate }}</span>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters'; import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
export default defineComponent({ const props = defineProps({
name: 'DateTimeTZ',
props: {
date: { required: true, type: Number }, date: { required: true, type: Number },
showTimezone: { required: false, type: Boolean, default: false }, showTimezone: { required: false, type: Boolean, default: false },
dateOnly: { required: false, type: Boolean, default: false }, dateOnly: { required: false, type: Boolean, default: false },
}, });
setup(props) {
const formattedDate = computed((): string => { const formattedDate = computed((): string => {
if (props.dateOnly) { if (props.dateOnly) {
return timestampToDateString(props.date); return timestampToDateString(props.date);
@ -34,9 +31,6 @@ export default defineComponent({
return `${time1}\n${timeUTC}`; return `${time1}\n${timeUTC}`;
}); });
return { formattedDate, timezoneTooltip };
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -0,0 +1,118 @@
<template>
<div class="d-flex flex-row">
<div class="flex-grow-1">
<slot v-if="!editing"> </slot>
<b-form-input v-else v-model="localName" size="sm"> </b-form-input>
</div>
<div
class="flex-grow-2 mt-auto d-flex gap-1 ms-1"
:class="alignVertical ? 'flex-column' : 'flex-row'"
>
<template v-if="allowEdit && !(addNew || editing)">
<b-button
size="sm"
variant="secondary"
:title="`Edit this ${editableName}.`"
@click="editing = true"
>
<i-mdi-pencil />
</b-button>
<b-button
size="sm"
variant="secondary"
:title="`Delete this ${editableName}.`"
@click="$emit('delete', modelValue)"
>
<i-mdi-delete />
</b-button>
</template>
<b-button
v-if="allowAdd && !(addNew || editing)"
size="sm"
:title="`Add new ${editableName}.`"
variant="primary"
@click="addNewClick"
><i-mdi-plus-box-outline />
</b-button>
<template v-if="addNew || editing">
<b-button
size="sm"
:title="`Add new '${editableName}`"
variant="primary"
@click="saveNewName"
>
<i-mdi-check />
</b-button>
<b-button size="sm" title="Abort" class="ms-1" variant="secondary" @click="abort">
<i-mdi-close />
</b-button>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps({
modelValue: {
type: String,
required: true,
},
allowEdit: {
type: Boolean,
default: false,
},
allowAdd: {
type: Boolean,
default: false,
},
editableName: {
type: String,
required: true,
},
alignVertical: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
(e: 'delete', value: string): void;
(e: 'new', value: string): void;
(e: 'rename', oldName: string, newName: string): void;
}>();
const addNew = ref(false);
const localName = ref<string>(props.modelValue);
const editing = ref<boolean>(false);
function abort() {
editing.value = false;
addNew.value = false;
localName.value = props.modelValue;
}
function addNewClick() {
localName.value = '';
addNew.value = true;
editing.value = true;
}
watch(
() => props.modelValue,
() => {
localName.value = props.modelValue;
},
);
function saveNewName() {
editing.value = false;
if (addNew.value) {
addNew.value = false;
emit('new', localName.value);
} else {
// Editing
emit('rename', props.modelValue, localName.value);
}
}
</script>

View File

@ -1,11 +1,10 @@
<template> <template>
<div :title="hint"> <div :title="hint">
<InfoIcon :size="18" /> <i-mdi-information-outline />
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import InfoIcon from 'vue-material-design-icons/InformationOutline.vue';
defineProps({ defineProps({
hint: { type: String, required: true }, hint: { type: String, required: true },
}); });

View File

@ -1,8 +1,12 @@
import ValuePair from '@/components/general/ValuePair.vue'; import ValuePair from '@/components/general/ValuePair.vue';
it('renders a message', () => { describe('ValuePair.vue', () => {
it('Renders a message', () => {
const msg = 'Test description'; const msg = 'Test description';
// https://github.com/cypress-io/cypress/issues/26628
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
cy.mount(ValuePair, { props: { description: msg } }); cy.mount(ValuePair, { props: { description: msg } });
cy.get('label').contains(msg); cy.get('label').contains(msg);
}); });
});

View File

@ -10,20 +10,14 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import InfoBox from '@/components/general/InfoBox.vue'; import InfoBox from '@/components/general/InfoBox.vue';
import { defineComponent } from 'vue'; defineProps({
export default defineComponent({
name: 'ValuePair',
components: { InfoBox },
props: {
description: { type: String, required: true }, description: { type: String, required: true },
help: { type: String, default: '', required: false }, help: { type: String, default: '', required: false },
classLabel: { type: String, default: 'col-4 fw-bold mb-0' }, classLabel: { type: String, default: 'col-4 fw-bold mb-0' },
classValue: { type: String, default: 'col-8' }, classValue: { type: String, default: 'col-8' },
},
}); });
</script> </script>

View File

@ -3,49 +3,54 @@
<!-- Only visible on xs (phone) viewport! --> <!-- Only visible on xs (phone) viewport! -->
<hr class="my-0" /> <hr class="my-0" />
<div class="d-flex flex-align-center justify-content-between px-2"> <div class="d-flex flex-align-center justify-content-between px-2">
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/open_trades"> <router-link
<OpenTradesIcon /> v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/open_trades"
>
<i-mdi-folder-open height="24" width="24" />
Trades Trades
</router-link> </router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/trade_history"> <router-link
<ClosedTradesIcon /> v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/trade_history"
>
<i-mdi-folder-lock height="24" width="24" />
History History
</router-link> </router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/pairlist"> <router-link
<PairListIcon /> v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/pairlist"
>
<i-mdi-view-list height="24" width="24" />
Pairlist Pairlist
</router-link> </router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/balance"> <router-link
<BalanceIcon /> v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/balance"
>
<i-mdi-bank height="24" width="24" />
Balance Balance
</router-link> </router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/dashboard"> <router-link
<DashboardIcon /> v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/dashboard"
>
<i-mdi-view-dashboard-outline height="24" width="24" />
Dashboard Dashboard
</router-link> </router-link>
</div> </div>
</footer> </footer>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import OpenTradesIcon from 'vue-material-design-icons/FolderOpen.vue';
import ClosedTradesIcon from 'vue-material-design-icons/FolderLock.vue';
import BalanceIcon from 'vue-material-design-icons/Bank.vue';
import PairListIcon from 'vue-material-design-icons/ViewList.vue';
import DashboardIcon from 'vue-material-design-icons/ViewDashboardOutline.vue';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'NavFooter',
components: { OpenTradesIcon, ClosedTradesIcon, BalanceIcon, PairListIcon, DashboardIcon },
setup() {
const botStore = useBotStore(); const botStore = useBotStore();
return {
botStore,
};
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -2,7 +2,8 @@ import { createPinia, PiniaVuePlugin } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { createApp } from 'vue'; import { createApp } from 'vue';
import App from './App.vue'; import App from './App.vue';
import { BootstrapVue3 } from './plugins/bootstrap-vue'; // Eensure Bootstrap css still loads
import './plugins/bootstrap-vue';
import { GridLayout } from './plugins/vue-grid-layout'; import { GridLayout } from './plugins/vue-grid-layout';
import router from './router'; import router from './router';
@ -14,7 +15,6 @@ pinia.use(piniaPluginPersistedstate);
myApp.use(pinia); myApp.use(pinia);
myApp.use(router); myApp.use(router);
myApp.use(BootstrapVue3);
myApp.use(GridLayout); myApp.use(GridLayout);
// Vue.config.productionTip = false; // Vue.config.productionTip = false;

View File

@ -1,7 +1,4 @@
import BootstrapVue3 from 'bootstrap-vue-next';
import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue-next/dist/bootstrap-vue-next.css'; import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
import '@/styles/main.scss'; import '@/styles/main.scss';
export { BootstrapVue3 };

View File

@ -18,7 +18,7 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: '/graph', path: '/graph',
name: 'Freqtrade Graph', name: 'Freqtrade Graph',
component: () => import('@/views/GraphsView.vue'), component: () => import('@/views/ChartsView.vue'),
}, },
{ {
path: '/logs', path: '/logs',

View File

@ -0,0 +1,50 @@
import { PlotConfig } from '@/types';
/**
* Calculate diff over 2 dataset columns and adds it to the end of the dataset
* modifies the incomming dataset array inplace!
*/
export function calculateDiff(
columns: string[],
data: number[][],
colFrom: string,
colTo: string,
): number[][] {
const fromIdx = columns.indexOf(colFrom);
const toIdx = columns.indexOf(colTo);
columns.push(`${colFrom}-${colTo}`);
return data.map((original) => {
// Prevent mutation of original data
const candle = original.slice();
const diff =
candle === null || candle[toIdx] === null || candle[fromIdx] === null
? null
: candle[toIdx] - candle[fromIdx];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
candle.push(diff);
return candle;
});
}
export function getDiffColumnsFromPlotConfig(plotConfig: PlotConfig): string[][] {
const result: string[][] = [];
if ('main_plot' in plotConfig) {
Object.entries(plotConfig.main_plot).forEach(([key, value]) => {
if (value.fill_to) {
result.push([key, value.fill_to]);
}
});
}
if ('subplots' in plotConfig) {
Object.values(plotConfig.subplots).forEach((subplots) => {
Object.entries(subplots).forEach(([key, value]) => {
if (value.fill_to) {
result.push([key, value.fill_to]);
}
});
});
}
return result;
}

View File

@ -0,0 +1,69 @@
import { ChartType, IndicatorConfig } from '@/types';
import { BarSeriesOption, LineSeriesOption, ScatterSeriesOption } from 'echarts';
import randomColor from '../randomColor';
export type SupportedSeriesTypes = LineSeriesOption | BarSeriesOption | ScatterSeriesOption;
export function generateCandleSeries(
colDate: number,
col: number,
key: string,
value: IndicatorConfig,
axis = 0,
): SupportedSeriesTypes {
const sp: SupportedSeriesTypes = {
name: key,
type: value.type || 'line',
xAxisIndex: axis,
yAxisIndex: axis,
itemStyle: {
color: value.color || randomColor(),
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
return sp;
}
export function generateAreaCandleSeries(
colDate: number,
fillCol: number,
key: string,
value: IndicatorConfig,
axis = 0,
): SupportedSeriesTypes {
const fillValue: IndicatorConfig = {
// color: value.color;
type: ChartType.line,
};
const areaSeries = generateCandleSeries(
colDate,
fillCol,
key,
fillValue,
axis,
) as LineSeriesOption;
const areaOptions: LineSeriesOption = {
stack: key,
stackStrategy: 'all',
lineStyle: {
opacity: 0,
},
showSymbol: false,
areaStyle: {
color: value.color,
opacity: 0.1,
},
tooltip: {
show: false, // hide value on tooltip
},
};
Object.assign(areaSeries, areaOptions);
return areaSeries;
}

View File

@ -1,4 +1,4 @@
export default function heikinAshiDataset(columns: string[], data: Array<number[]>) { export function heikinAshiDataset(columns: string[], data: Array<number[]>): number[][] {
const openIdx = columns.indexOf('open'); const openIdx = columns.indexOf('open');
const closeIdx = columns.indexOf('close'); const closeIdx = columns.indexOf('close');
const highIdx = columns.indexOf('high'); const highIdx = columns.indexOf('high');
@ -26,3 +26,5 @@ export default function heikinAshiDataset(columns: string[], data: Array<number[
return candle; return candle;
}); });
} }
export default heikinAshiDataset;

3
src/shared/deepClone.ts Normal file
View File

@ -0,0 +1,3 @@
export function deepClone<T>(object: T): T {
return JSON.parse(JSON.stringify(object));
}

View File

@ -1,35 +0,0 @@
import { PlotConfig, EMPTY_PLOTCONFIG, PlotConfigStorage } from '@/types';
const PLOT_CONFIG = 'ft_custom_plot_config';
const PLOT_CONFIG_NAME = 'ft_selected_plot_config';
export function getPlotConfigName(): string {
return localStorage.getItem(PLOT_CONFIG_NAME) || 'default';
}
export function storePlotConfigName(plotConfigName: string): void {
localStorage.setItem(PLOT_CONFIG_NAME, plotConfigName);
}
export function getAllCustomPlotConfig(): PlotConfig {
return JSON.parse(localStorage.getItem(PLOT_CONFIG) || '{}');
}
export function getAllPlotConfigNames(): Array<string> {
return Object.keys(getAllCustomPlotConfig());
}
export function getCustomPlotConfig(configName: string): PlotConfig {
const configs = getAllCustomPlotConfig();
return configName in configs ? configs[configName] : { ...EMPTY_PLOTCONFIG };
}
export function storeCustomPlotConfig(plotConfig: PlotConfigStorage) {
const existingConfig = getAllCustomPlotConfig();
// Merge existing with new config
const finalPlotConfig = { ...existingConfig, ...plotConfig };
localStorage.setItem(PLOT_CONFIG, JSON.stringify(finalPlotConfig));
// Store new config name as default
storePlotConfigName(Object.keys(plotConfig)[0]);
}

View File

@ -99,14 +99,17 @@ export class UserService {
public static getAvailableBots(): BotDescriptors { public static getAvailableBots(): BotDescriptors {
const allInfo = UserService.getAllLoginInfos(); const allInfo = UserService.getAllLoginInfos();
const response: BotDescriptors = {}; const response: BotDescriptors = {};
Object.entries(allInfo).forEach(([k, v], idx) => { Object.keys(allInfo)
.sort((a, b) => (allInfo[a].sortId ?? 0) - (allInfo[b].sortId ?? 0))
.forEach((k, idx) => {
response[k] = { response[k] = {
botId: k, botId: k,
botName: v.botName, botName: allInfo[k].botName,
botUrl: v.apiUrl, botUrl: allInfo[k].apiUrl,
sortId: v.sortId ?? idx, sortId: allInfo[k].sortId ?? idx,
}; };
}); });
return response; return response;
} }

View File

@ -520,7 +520,7 @@ export function createBotSubStore(botId: string, botName: string) {
}, },
async getState() { async getState() {
try { try {
const { data } = await api.get('/show_config'); const { data } = await api.get<BotState>('/show_config');
this.botState = data; this.botState = data;
this.botStatusAvailable = true; this.botStatusAvailable = true;
this.startWebSocket(); this.startWebSocket();
@ -654,6 +654,18 @@ export function createBotSubStore(botId: string, botName: string) {
return Promise.reject(error); return Promise.reject(error);
} }
}, },
async reloadTrade(tradeid: string) {
try {
const res = await api.post<never, AxiosResponse<Trade>>(`/trades/${tradeid}/reload`);
return Promise.resolve(res);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.response);
}
showAlert(`Failed to reload trade ${tradeid}`, 'danger');
return Promise.reject(error);
}
},
async startTrade() { async startTrade() {
try { try {
const res = await api.post('/start_trade', {}); const res = await api.post('/start_trade', {});
@ -692,10 +704,7 @@ export function createBotSubStore(botId: string, botName: string) {
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.error(error.response); console.error(error.response);
showAlert( showAlert(`Error occured entering: '${error.response?.data?.error}'`, 'danger');
`Error occured entering: '${(error as any).response?.data?.error}'`,
'danger',
);
} }
return Promise.reject(error); return Promise.reject(error);
} }
@ -730,9 +739,7 @@ export function createBotSubStore(botId: string, botName: string) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.error(error.response); console.error(error.response);
showAlert( showAlert(
`Error occured while adding pairs to Blacklist: '${ `Error occured while adding pairs to Blacklist: '${error.response?.data?.error}'`,
(error as any).response?.data?.error
}'`,
'danger', 'danger',
); );
} }
@ -778,9 +785,7 @@ export function createBotSubStore(botId: string, botName: string) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.error(error.response); console.error(error.response);
showAlert( showAlert(
`Error occured while removing pairs from Blacklist: '${ `Error occured while removing pairs from Blacklist: '${error.response?.data?.error}'`,
(error as any).response?.data?.error
}'`,
'danger', 'danger',
); );
} }
@ -876,6 +881,7 @@ export function createBotSubStore(botId: string, botName: string) {
return Promise.reject(err); return Promise.reject(err);
} }
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_handleWebsocketMessage(ws, event: MessageEvent<any>) { _handleWebsocketMessage(ws, event: MessageEvent<any>) {
const msg: FTWsMessage = JSON.parse(event.data); const msg: FTWsMessage = JSON.parse(event.data);
switch (msg.type) { switch (msg.type) {
@ -899,6 +905,7 @@ export function createBotSubStore(botId: string, botName: string) {
} }
default: default:
// Unhandled events ... // Unhandled events ...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.log(`Received event ${(msg as any).type}`); console.log(`Received event ${(msg as any).type}`);
break; break;
} }

View File

@ -8,8 +8,10 @@ import {
DailyPayload, DailyPayload,
DailyRecord, DailyRecord,
DailyReturnValue, DailyReturnValue,
MultiCancelOpenOrderPayload,
MultiDeletePayload, MultiDeletePayload,
MultiForcesellPayload, MultiForcesellPayload,
MultiReloadTradePayload,
ProfitInterface, ProfitInterface,
Trade, Trade,
} from '@/types'; } from '@/types';
@ -234,7 +236,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
// Ensure all bots status is correct. // Ensure all bots status is correct.
await this.pingAll(); await this.pingAll();
const botStoreUpdates: Promise<any>[] = []; const botStoreUpdates: Promise<BotState>[] = [];
this.allBotStores.forEach((bot) => { this.allBotStores.forEach((bot) => {
if (bot.isBotOnline && !bot.botStatusAvailable) { if (bot.isBotOnline && !bot.botStatusAvailable) {
botStoreUpdates.push(bot.getState()); botStoreUpdates.push(bot.getState());
@ -283,7 +285,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
}, },
async pingAll() { async pingAll() {
await Promise.all( await Promise.all(
Object.entries(this.botStores).map(async ([_, v]) => { Object.values(this.botStores).map(async (v) => {
try { try {
await v.fetchPing(); await v.fetchPing();
} catch { } catch {
@ -293,7 +295,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
); );
}, },
allGetState() { allGetState() {
Object.entries(this.botStores).map(async ([_, v]) => { Object.values(this.botStores).map(async (v) => {
try { try {
await v.getState(); await v.getState();
} catch { } catch {
@ -317,9 +319,12 @@ export const useBotStore = defineStore('ftbot-wrapper', {
async deleteTradeMulti(deletePayload: MultiDeletePayload) { async deleteTradeMulti(deletePayload: MultiDeletePayload) {
return this.botStores[deletePayload.botId].deleteTrade(deletePayload.tradeid); return this.botStores[deletePayload.botId].deleteTrade(deletePayload.tradeid);
}, },
async cancelOpenOrderMulti(deletePayload: MultiDeletePayload) { async cancelOpenOrderMulti(deletePayload: MultiCancelOpenOrderPayload) {
return this.botStores[deletePayload.botId].cancelOpenOrder(deletePayload.tradeid); return this.botStores[deletePayload.botId].cancelOpenOrder(deletePayload.tradeid);
}, },
async reloadTradeMulti(deletePayload: MultiReloadTradePayload) {
return this.botStores[deletePayload.botId].reloadTrade(deletePayload.tradeid);
},
}, },
}); });

View File

@ -1,43 +1,80 @@
import { import { deepClone } from '@/shared/deepClone';
getAllPlotConfigNames, import { EMPTY_PLOTCONFIG, PlotConfig, PlotConfigStorage } from '@/types';
getCustomPlotConfig,
getPlotConfigName,
storeCustomPlotConfig,
storePlotConfigName,
} from '@/shared/storage';
import { PlotConfigStorage, EMPTY_PLOTCONFIG, PlotConfig } from '@/types';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
const FT_PLOT_CONFIG_KEY = 'ftPlotConfig';
function migratePlotConfigs() {
// Legacy config names
const PLOT_CONFIG = 'ft_custom_plot_config';
const PLOT_CONFIG_NAME = 'ft_selected_plot_config';
const allConfigs = JSON.parse(localStorage.getItem(PLOT_CONFIG) || '{}');
if (Object.keys(allConfigs).length > 0) {
console.log('migrating plot configs');
const res = {
customPlotConfigs: allConfigs,
plotConfigName: localStorage.getItem(PLOT_CONFIG_NAME) || 'default',
};
localStorage.setItem(FT_PLOT_CONFIG_KEY, JSON.stringify(res));
localStorage.removeItem(PLOT_CONFIG);
localStorage.removeItem(PLOT_CONFIG_NAME);
}
}
migratePlotConfigs();
export const usePlotConfigStore = defineStore('plotConfig', { export const usePlotConfigStore = defineStore('plotConfig', {
state: () => { state: () => {
return { return {
customPlotConfig: {} as PlotConfigStorage, customPlotConfigs: {} as PlotConfigStorage,
plotConfigName: getPlotConfigName(), plotConfigName: 'default',
availablePlotConfigNames: getAllPlotConfigNames(), isEditing: false,
plotConfig: { ...EMPTY_PLOTCONFIG }, editablePlotConfig: { ...EMPTY_PLOTCONFIG } as PlotConfig,
}; };
}, },
getters: { getters: {
availablePlotConfigNames: (state) => Object.keys(state.customPlotConfigs),
plotConfig: (state) =>
(state.isEditing
? state.editablePlotConfig
: state.customPlotConfigs[state.plotConfigName]) || deepClone(EMPTY_PLOTCONFIG),
// plotConfig: (state) => state.customPlotConfig[state.plotConfigName] || { ...EMPTY_PLOTCONFIG }, // plotConfig: (state) => state.customPlotConfig[state.plotConfigName] || { ...EMPTY_PLOTCONFIG },
}, },
actions: { actions: {
saveCustomPlotConfig(plotConfig: PlotConfigStorage) { saveCustomPlotConfig(name: string, plotConfig: PlotConfig) {
this.customPlotConfig = plotConfig; // This will autosave to storage due to pinia-persist
storeCustomPlotConfig(plotConfig); this.customPlotConfigs[name] = plotConfig;
this.availablePlotConfigNames = getAllPlotConfigNames();
}, },
setPlotConfigName(plotConfigName: string) { deletePlotConfig(plotConfigName: string) {
delete this.customPlotConfigs[plotConfigName];
if (this.plotConfigName === plotConfigName) {
this.plotConfigName =
this.availablePlotConfigNames[this.availablePlotConfigNames.length - 1];
}
},
renamePlotConfig(oldName: string, newName: string) {
this.customPlotConfigs[newName] = this.customPlotConfigs[oldName];
delete this.customPlotConfigs[oldName];
this.plotConfigName = newName;
},
newPlotConfig(plotConfigName: string) {
this.editablePlotConfig = deepClone(EMPTY_PLOTCONFIG);
this.saveCustomPlotConfig(plotConfigName, this.editablePlotConfig);
this.plotConfigName = plotConfigName; this.plotConfigName = plotConfigName;
storePlotConfigName(plotConfigName);
}, },
plotConfigChanged(plotConfigName = '') { plotConfigChanged(plotConfigName = '') {
console.log('plotConfigChanged'); if (plotConfigName) {
this.setPlotConfigName(plotConfigName ? plotConfigName : this.plotConfigName); this.plotConfigName = plotConfigName;
this.plotConfig = getCustomPlotConfig(this.plotConfigName); }
console.log('plotConfigChanged', this.plotConfigName);
if (this.isEditing) {
this.editablePlotConfig = deepClone(this.customPlotConfigs[this.plotConfigName]);
}
}, },
setPlotConfig(plotConfig: PlotConfig) {
console.log('emit...');
this.plotConfig = { ...plotConfig };
}, },
persist: {
key: FT_PLOT_CONFIG_KEY,
paths: ['plotConfigName', 'customPlotConfigs'],
}, },
}); });

View File

@ -13,7 +13,7 @@
} }
.v-select * { .v-select * {
font-size: 0.8rem; font-size: 0.8rem !important;
} }
.modal.show { .modal.show {
@ -22,13 +22,21 @@
} }
.btn-primary { .btn-primary {
color: #ffffff color: #ffffff !important;
} }
.text-bg-primary { .text-bg-primary {
color: #ffffff !important; color: #ffffff !important;
} }
.card {
padding: 0px;
}
.vs__open-indicator {
transform: none !important;
}
[data-theme="dark"] { [data-theme="dark"] {
$bg-dark: rgb(18, 18, 18); $bg-dark: rgb(18, 18, 18);
@ -133,8 +141,9 @@
} }
// Styles for searchable select // Styles for searchable select
.vs__dropdown-toggle { .vs__dropdown-toggle, .vs__clear {
border-color: lighten($bg-dark, 20%); border-color: lighten($bg-dark, 20%);
color: $fg-color;
// border: 1px solid $fg-color; // border: 1px solid $fg-color;
} }
@ -189,7 +198,11 @@
.form-select { .form-select {
color: $fg-color; color: $fg-color;
border-color: lighten($bg-dark, 20%); border-color: lighten($bg-dark, 20%);
background: $bg-dark url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23dedede' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px background: $bg-dark;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dedede' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
background-size: 16px 12px;
background-repeat: no-repeat;
background-position: right 0.75rem center;
} }
.b-toast .toast { .b-toast .toast {

View File

@ -50,14 +50,32 @@ export interface ExitReasonResults {
wins: number; wins: number;
} }
// Generated by https://quicktype.io
export interface PeriodicStat {
date: string;
date_ts: number;
profit_abs: number;
wins: number;
draws: number;
loses: number;
}
export interface PeriodicBreakdown {
day: PeriodicStat[];
week: PeriodicStat[];
month: PeriodicStat[];
}
export interface StrategyBacktestResult { export interface StrategyBacktestResult {
trades: ClosedTrade[]; trades: ClosedTrade[];
locks: Lock[]; locks: Lock[];
best_pair: PairResult; best_pair: PairResult;
worst_pair: PairResult; worst_pair: PairResult;
results_per_pair: Array<PairResult>; results_per_pair: PairResult[];
sell_reason_summary?: Array<ExitReasonResults>; sell_reason_summary?: ExitReasonResults[];
exit_reason_summary?: Array<ExitReasonResults>; exit_reason_summary?: ExitReasonResults[];
periodic_breakdown?: PeriodicBreakdown;
left_open_trades: Trade[]; left_open_trades: Trade[];
total_trades: number; total_trades: number;
total_volume: number; total_volume: number;

View File

@ -1,17 +1,18 @@
export interface BalanceRecords { export interface BalanceRecords {
[key: string]: string | number;
balance: number; balance: number;
currency: string; currency: string;
est_stake: number; est_stake: number;
est_stake_bot?: number;
free: number; free: number;
used: number; used: number;
bot_owned?: number;
stake: string; stake: string;
// Properties added in v 2.x // Properties added in v 2.x
// Temporarily disabled to fix type errors side: string;
// side: string; leverage: number;
// leverage: number; is_position: boolean;
// is_position: boolean; position: number;
// position: number; is_bot_managed?: boolean;
} }
export interface BalanceInterface { export interface BalanceInterface {
@ -21,10 +22,14 @@ export interface BalanceInterface {
stake: string; stake: string;
/** Fiat symbol used */ /** Fiat symbol used */
symbol: string; symbol: string;
/** Total Balance in stake currency */ /** Total Account Balance in stake currency */
total: number; total: number;
/** Balance in FIAT currency */ /** Total Bot Balance in stake currency */
total_bot?: number;
/** Account Balance in FIAT currency */
value: number; value: number;
/** Bot Balance in FIAT currency */
value_bot?: number;
/** Assumed starting capital */ /** Assumed starting capital */
starting_capital: number; starting_capital: number;
/** Change between starting capital and current value */ /** Change between starting capital and current value */
@ -36,3 +41,13 @@ export interface BalanceInterface {
starting_capital_fiat_ratio: number; starting_capital_fiat_ratio: number;
starting_capital_fiat_pct: number; starting_capital_fiat_pct: number;
} }
export interface BalanceValues {
[key: string]: number | string;
balance: number;
currency: string;
est_stake: number;
free: number;
used: number;
stake: string;
}

View File

@ -7,6 +7,12 @@ export interface CumProfitDataPerDate {
[key: number]: CumProfitData; [key: number]: CumProfitData;
} }
export type CumProfitChartData = {
date: number;
profit?: number;
currentProfit?: number;
};
export interface ChartSliderPosition { export interface ChartSliderPosition {
startValue: number; startValue: number;
endValue: number | undefined; endValue: number | undefined;

View File

@ -7,6 +7,7 @@ export enum ChartType {
export interface IndicatorConfig { export interface IndicatorConfig {
color?: string; color?: string;
type?: ChartType; type?: ChartType;
fill_to?: string;
} }
export interface PlotConfig { export interface PlotConfig {

View File

@ -26,8 +26,10 @@ export interface ProfitInterface {
trade_count: number; trade_count: number;
closed_trade_count: number; closed_trade_count: number;
first_trade_date: string; first_trade_date: string;
first_trade_humanized: string;
first_trade_timestamp: number; first_trade_timestamp: number;
latest_trade_date: string; latest_trade_date: string;
latest_trade_humanized: string;
latest_trade_timestamp: number; latest_trade_timestamp: number;
avg_duration: string; avg_duration: string;
best_pair: string; best_pair: string;

View File

@ -134,7 +134,7 @@ export interface ClosedTrade extends TradeBase {
fee_close_cost?: number; fee_close_cost?: number;
fee_close_currency?: string; fee_close_currency?: string;
sell_reason: string; exit_reason: string;
min_rate: number; min_rate: number;
max_rate: number; max_rate: number;
} }

View File

@ -25,11 +25,15 @@ export interface MultiForcesellPayload extends ForceSellPayload {
} }
/** Interface only used internally to ensure the right bot is being called in a multibot environment. */ /** Interface only used internally to ensure the right bot is being called in a multibot environment. */
export interface MultiDeletePayload { export interface MultiBotIdPayload {
tradeid: string; tradeid: string;
botId: string; botId: string;
} }
export type MultiDeletePayload = MultiBotIdPayload;
export type MultiReloadTradePayload = MultiBotIdPayload;
export type MultiCancelOpenOrderPayload = MultiBotIdPayload;
export interface PerformanceEntry { export interface PerformanceEntry {
count: number; count: number;
pair: string; pair: string;
@ -121,8 +125,7 @@ export interface EntryPricing extends PriceBase {
export interface BotState { export interface BotState {
version: string; version: string;
strategy_version?: string; strategy_version?: string;
/** Api version - was not provided prior to 1.1 (or 2021.11) */ api_version: number;
api_version?: number;
dry_run: boolean; dry_run: boolean;
/** Futures, margin or spot */ /** Futures, margin or spot */
trading_mode?: TradingMode; trading_mode?: TradingMode;
@ -136,8 +139,6 @@ export interface BotState {
unfilledtimeout: UnfilledTimeout; unfilledtimeout: UnfilledTimeout;
order_types: OrderTypes; order_types: OrderTypes;
exchange: string; exchange: string;
/** @deprecated replaced by force_entry_enable in 2.x */
forcebuy_enabled?: boolean;
force_entry_enable?: boolean; force_entry_enable?: boolean;
max_open_trades: number; max_open_trades: number;
minimal_roi: object; minimal_roi: object;
@ -206,6 +207,7 @@ export interface PairHistoryPayload {
timeframe: string; timeframe: string;
timerange: string; timerange: string;
strategy: string; strategy: string;
freqaimodel?: string;
} }
export interface PairHistory { export interface PairHistory {
@ -214,7 +216,7 @@ export interface PairHistory {
timeframe: string; timeframe: string;
timeframe_ms: number; timeframe_ms: number;
columns: string[]; columns: string[];
data: Array<number[]>; data: number[][];
length: number; length: number;
/** Number of buy signals in this response */ /** Number of buy signals in this response */
buy_signals: number; buy_signals: number;

View File

@ -316,6 +316,7 @@
:timerange="timerange" :timerange="timerange"
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist" :pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
:trades="botStore.activeBot.selectedBacktestResult.trades" :trades="botStore.activeBot.selectedBacktestResult.trades"
:freqai-model="freqAI.enabled ? freqAI.model : undefined"
/> />
</div> </div>
</div> </div>

View File

@ -86,7 +86,11 @@
drag-allow-from=".drag-header" drag-allow-from=".drag-header"
> >
<DraggableContainer header="Cumulative Profit"> <DraggableContainer header="Cumulative Profit">
<CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" /> <CumProfitChart
:trades="botStore.allTradesSelectedBots"
:open-trades="botStore.allOpenTradesSelectedBots"
:show-title="false"
/>
</DraggableContainer> </DraggableContainer>
</grid-item> </grid-item>
<grid-item <grid-item

View File

@ -8,7 +8,3 @@
</p> </p>
</div> </div>
</template> </template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -4,14 +4,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import LogViewer from '@/components/ftbot/LogViewer.vue'; import LogViewer from '@/components/ftbot/LogViewer.vue';
export default defineComponent({
name: 'LogView',
components: { LogViewer },
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,8 +1,6 @@
<template> <template>
<div> <div>
<b-button @click="openLoginModal()" <b-button @click="openLoginModal()"><i-mdi-login class="me-1" />{{ loginText }}</b-button>
><LoginIcon :size="16" class="me-1" />{{ loginText }}</b-button
>
<b-modal <b-modal
id="modal-prevent-closing" id="modal-prevent-closing"
v-model="loginViewOpen" v-model="loginViewOpen"
@ -23,7 +21,6 @@
import Login from '@/components/BotLogin.vue'; import Login from '@/components/BotLogin.vue';
import { AuthStorageWithBotId } from '@/types'; import { AuthStorageWithBotId } from '@/types';
import { nextTick, ref } from 'vue'; import { nextTick, ref } from 'vue';
import LoginIcon from 'vue-material-design-icons/Login.vue';
defineProps({ defineProps({
loginText: { required: false, default: 'Login', type: String }, loginText: { required: false, default: 'Login', type: String },

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="container"> <div class="container">
<b-card header="Freqtrade bot Login"> <b-card header="Freqtrade bot Login">
<Login ref="loginForm" /> <BotLogin ref="loginForm" />
</b-card> </b-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Login from '@/components/BotLogin.vue'; import BotLogin from '@/components/BotLogin.vue';
</script> </script>
<style scoped> <style scoped>

View File

@ -30,7 +30,7 @@
size="sm" size="sm"
class="align-self-start mt-1 ms-1" class="align-self-start mt-1 ms-1"
@click="botStore.activeBot.setDetailTrade(null)" @click="botStore.activeBot.setDetailTrade(null)"
><BackIcon /> Back</b-button ><i-mdi-arrow-left /> Back</b-button
> >
<TradeDetail <TradeDetail
:trade="botStore.activeBot.tradeDetail" :trade="botStore.activeBot.tradeDetail"
@ -40,31 +40,15 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import CustomTradeList from '@/components/ftbot/CustomTradeList.vue'; import CustomTradeList from '@/components/ftbot/CustomTradeList.vue';
import TradeDetail from '@/components/ftbot/TradeDetail.vue'; import TradeDetail from '@/components/ftbot/TradeDetail.vue';
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper'; import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({ defineProps({
name: 'TradesList',
components: {
CustomTradeList,
TradeDetail,
BackIcon,
},
props: {
history: { default: false, type: Boolean }, history: { default: false, type: Boolean },
},
setup() {
const botStore = useBotStore();
return {
botStore,
};
},
}); });
const botStore = useBotStore();
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -16,6 +16,7 @@
"types": [ "types": [
"cypress", "cypress",
"vite/client", "vite/client",
"unplugin-icons/types/vue",
], ],
"paths": { "paths": {
"@/*": [ "@/*": [

View File

@ -1,10 +1,24 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import createVuePlugin from '@vitejs/plugin-vue'; import createVuePlugin from '@vitejs/plugin-vue';
import { resolve } from 'path'; import { resolve } from 'path';
import Icons from 'unplugin-icons/vite';
import Components from 'unplugin-vue-components/vite';
import IconsResolve from 'unplugin-icons/resolver';
import { BootstrapVueNextResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [createVuePlugin({})], plugins: [
createVuePlugin({}),
Components({
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
dirs: [],
dts: true,
}),
Icons({
compiler: 'vue3',
}),
],
resolve: { resolve: {
dedupe: ['vue'], dedupe: ['vue'],
alias: { alias: {
@ -29,6 +43,7 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
}, },
}, },
host: '127.0.0.1',
port: 3000, port: 3000,
}, },
}); });

1023
yarn.lock

File diff suppressed because it is too large Load Diff