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

@ -1,35 +1,41 @@
/* cSpell:disable */ /* cSpell:disable */
{ {
"name": "frequi", "name": "frequi",
"build": { "build": {
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },
"forwardPorts": [ "forwardPorts": [
3000 3000
], ],
"mounts": [ "mounts": [
"source=frequi-bashhistory,target=/home/node/commandhistory,type=volume" "source=frequi-bashhistory,target=/home/node/commandhistory,type=volume"
], ],
"remoteUser": "node", "remoteUser": "node",
"settings": { "customizations": {
// "editor.formatOnSave": true, "vscode": {
"editor.codeActionsOnSave": { "settings": {
"source.fixAll.eslint": true // "editor.formatOnSave": true,
}, "editor.codeActionsOnSave": {
"emmet.includeLanguages": { "source.fixAll.eslint": true
"vue": "html", },
"vue-html": "html" "emmet.includeLanguages": {
}, "vue": "html",
"workbench.iconTheme": "vscode-icons", "vue-html": "html"
}, },
"extensions": [ "workbench.iconTheme": "vscode-icons"
"vue.volar", },
"dbaeumer.vscode-eslint", "extensions": [
"yzhang.markdown-all-in-one", "vue.volar",
"marquesmps.dockerfile-validator", "dbaeumer.vscode-eslint",
"streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one",
"vscode-icons-team.vscode-icons", "marquesmps.dockerfile-validator",
"hediet.vscode-drawio", "streetsidesoftware.code-spell-checker",
], "vscode-icons-team.vscode-icons",
"postCreateCommand": "yarn install", "hediet.vscode-drawio",
"ZixuanChen.vitest-explorer",
"antfu.iconify"
]
}
},
"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,31 +6,24 @@
</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({ const settingsStore = useSettingsStore();
name: 'App', onMounted(() => {
components: { NavBar, BodyLayout, NavFooter }, setTimezone(settingsStore.timezone);
setup() {
const settingsStore = useSettingsStore();
onMounted(() => {
setTimezone(settingsStore.timezone);
});
watch(
() => settingsStore.timezone,
(tz) => {
console.log('timezone changed', tz);
setTimezone(tz);
},
);
return {};
},
}); });
watch(
() => settingsStore.timezone,
(tz) => {
console.log('timezone changed', tz);
setTimezone(tz);
},
);
</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"
class="ms-2 me-1 align-middle"
:class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'"
:title="botStore.botStores[bot.botId].isBotOnline ? 'Online' : 'Offline'" :title="botStore.botStores[bot.botId].isBotOnline ? 'Online' : 'Offline'"
></OnlineIcon> >
<LoggedOutIcon <i-mdi-circle
v-else class="ms-2 me-1 align-middle"
class="offline" :class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'"
title="Login info expired, please login again." />
></LoggedOutIcon> </div>
<div v-else title="Login info expired, please login again.">
<i-mdi-cancel class="offline" />
</div>
</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,59 +54,34 @@
</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', bot: { required: true, type: Object as () => BotDescriptor },
components: { noButtons: { default: false, type: Boolean },
DeleteIcon, });
EditIcon, defineEmits(['edit', 'editLogin']);
LoginIcon, const botStore = useBotStore();
OnlineIcon,
LoggedOutIcon, const changeEvent = (v) => {
botStore.botStores[props.bot.botId].setAutoRefresh(v);
};
const botRemoveModalVisible = ref(false);
const confirmRemoveBot = () => {
botRemoveModalVisible.value = false;
botStore.removeBot(props.bot.botId);
console.log('removing bot.');
};
const autoRefreshLoc = computed({
get() {
return botStore.botStores[props.bot.botId].autoRefresh;
}, },
props: { set() {
bot: { required: true, type: Object as () => BotDescriptor }, // pass
noButtons: { default: false, type: Boolean },
},
emits: ['edit', 'editLogin'],
setup(props) {
const botStore = useBotStore();
const changeEvent = (v) => {
botStore.botStores[props.bot.botId].setAutoRefresh(v);
};
const botRemoveModalVisible = ref(false);
const confirmRemoveBot = () => {
botRemoveModalVisible.value = false;
botStore.removeBot(props.bot.botId);
console.log('removing bot.');
};
const autoRefreshLoc = computed({
get() {
return botStore.botStores[props.bot.botId].autoRefresh;
},
set() {
// pass
},
});
return {
botStore,
changeEvent,
autoRefreshLoc,
confirmRemoveBot,
botRemoveModalVisible,
};
}, },
}); });
</script> </script>

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,49 +11,34 @@
<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', bot: { type: Object as () => BotDescriptor, required: true },
components: {
CheckIcon,
CloseIcon,
},
props: {
bot: { type: Object as () => BotDescriptor, required: true },
},
emits: ['cancelled', 'saved'],
setup(props, { emit }) {
const botStore = useBotStore();
const newName = ref<string>(props.bot.botName);
const save = () => {
botStore.updateBot(props.bot.botId, {
botName: newName.value,
});
emit('saved');
};
return {
newName,
save,
};
},
}); });
const emit = defineEmits(['cancelled', 'saved']);
const botStore = useBotStore();
const newName = ref<string>(props.bot.botName);
const save = () => {
botStore.updateBot(props.bot.botId, {
botName: newName.value,
});
emit('saved');
};
</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,67 +37,57 @@ use([
LabelLayout, LabelLayout,
]); ]);
export default defineComponent({ const props = defineProps({
name: 'BalanceChart', currencies: { required: true, type: Array as () => BalanceValues[] },
components: { showTitle: { required: false, type: Boolean },
'v-chart': ECharts, });
}, const settingsStore = useSettingsStore();
props: {
currencies: { required: true, type: Array as () => BalanceRecords[] },
showTitle: { required: false, type: Boolean },
},
setup(props) {
const settingsStore = useSettingsStore();
const balanceChartOptions = computed((): EChartsOption => { const balanceChartOptions = computed((): EChartsOption => {
return { return {
title: { title: {
text: 'Balance', text: 'Balance',
show: props.showTitle, show: props.showTitle,
},
center: ['50%', '50%'],
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['balance', 'currency', 'est_stake', 'free', 'used', 'stake'],
source: props.currencies,
},
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${formatPriceCurrency(params.value.balance, params.value.currency, 8)}<br />${
params.percent
}% (${formatPriceCurrency(params.value.est_stake, params.value.stake)})`;
},
},
// legend: {
// orient: 'vertical',
// right: 10,
// top: 20,
// bottom: 20,
// },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
encode: {
value: 'est_stake',
itemName: 'currency',
tooltip: ['balance', 'currency'],
}, },
center: ['50%', '50%'], label: {
backgroundColor: 'rgba(0, 0, 0, 0)', formatter: '{b} - {d}%',
dataset: {
dimensions: ['balance', 'currency', 'est_stake', 'free', 'used', 'stake'],
source: props.currencies,
}, },
tooltip: { tooltip: {
trigger: 'item', show: true,
formatter: (params) => {
return `${formatPriceCurrency(params.value.balance, params.value.currency, 8)}<br />${
params.percent
}% (${formatPriceCurrency(params.value.est_stake, params.value.stake)})`;
},
}, },
// legend: { },
// orient: 'vertical', ],
// right: 10, };
// top: 20,
// bottom: 20,
// },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
encode: {
value: 'est_stake',
itemName: 'currency',
tooltip: ['balance', 'currency'],
},
label: {
formatter: '{b} - {d}%',
},
tooltip: {
show: true,
},
},
],
};
});
return { balanceChartOptions, settingsStore };
},
}); });
</script> </script>

File diff suppressed because it is too large Load Diff

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,158 +48,226 @@ 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', trades: { required: true, type: Array as () => ClosedTrade[] },
components: { openTrades: { required: true, type: Array as () => Trade[] },
'v-chart': ECharts, showTitle: { default: true, type: Boolean },
}, profitColumn: { default: 'profit_abs', type: String },
props: { });
trades: { required: true, type: Array as () => ClosedTrade[] }, const settingsStore = useSettingsStore();
showTitle: { default: true, type: Boolean }, // const botList = ref<string[]>([]);
profitColumn: { default: 'profit_abs', type: String }, // const cumulativeData = ref<{ date: number; profit: any }[]>([]);
},
setup(props) {
const settingsStore = useSettingsStore();
// const botList = ref<string[]>([]);
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
const cumulativeData: ComputedRef<{ date: number; profit: number }[]> = computed(() => { const chart = ref<typeof ECharts>();
const res: CumProfitData[] = [];
const resD: CumProfitDataPerDate = {};
const closedTrades = props.trades
.slice()
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
let profit = 0.0;
for (let i = 0, len = closedTrades.length; i < len; i += 1) { const openProfit = computed<number>(() => {
const trade = closedTrades[i]; return props.openTrades.reduce((a, v) => a + v[props.profitColumn], 0);
});
if (trade.close_timestamp && trade[props.profitColumn]) { const cumulativeData = computed<CumProfitChartData[]>(() => {
profit += trade[props.profitColumn]; const res: CumProfitData[] = [];
if (!resD[trade.close_timestamp]) { const resD: CumProfitDataPerDate = {};
// New timestamp const closedTrades = props.trades
resD[trade.close_timestamp] = { profit, [trade.botId]: profit }; .slice()
} else { .sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
// Add to existing profit let profit = 0.0;
resD[trade.close_timestamp].profit += trade[props.profitColumn];
if (resD[trade.close_timestamp][trade.botId]) { for (let i = 0, len = closedTrades.length; i < len; i += 1) {
resD[trade.close_timestamp][trade.botId] += trade[props.profitColumn]; const trade = closedTrades[i];
} else {
resD[trade.close_timestamp][trade.botId] = profit; if (trade.close_timestamp && trade[props.profitColumn]) {
} profit += trade[props.profitColumn];
} if (!resD[trade.close_timestamp]) {
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit }); // New timestamp
resD[trade.close_timestamp] = { profit, [trade.botId]: profit };
} else {
// Add to existing profit
resD[trade.close_timestamp].profit += trade[props.profitColumn];
if (resD[trade.close_timestamp][trade.botId]) {
resD[trade.close_timestamp][trade.botId] += trade[props.profitColumn];
} else {
resD[trade.close_timestamp][trade.botId] = profit;
} }
} }
// console.log(resD); res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
}
}
return Object.entries(resD).map(([k, v]) => { const valueArray: CumProfitChartData[] = Object.entries(resD).map(
const obj = { date: parseInt(k, 10), profit: v.profit }; ([k, v]: [string, CumProfitData]) => {
// TODO: The below could allow "lines" per bot" const obj = { date: parseInt(k, 10), profit: v.profit };
// this.botList.forEach((botId) => { // TODO: The below could allow "lines" per bot"
// obj[botId] = v[botId]; // this.botList.forEach((botId) => {
// }); // obj[botId] = v[botId];
return obj;
});
});
const chartOptions = computed((): EChartsOption => {
const chartOptionsLoc: EChartsOption = {
title: {
text: 'Cumulative Profit',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'profit'],
source: cumulativeData.value,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [CHART_PROFIT],
right: '5%',
},
useUTC: false,
xAxis: {
type: 'time',
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 40,
},
],
grid: {
bottom: 80,
},
dataZoom: [
{
type: 'inside',
// xAxisIndex: [0],
start: 0,
end: 100,
},
{
// xAxisIndex: [0],
bottom: 10,
start: 0,
end: 100,
...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?
// 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',
// });
// }); // });
return chartOptionsLoc; return obj;
}); },
);
return { settingsStore, cumulativeData, chartOptions }; 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;
}); });
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 = {
title: {
text: 'Cumulative Profit',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [CHART_PROFIT],
right: '5%',
},
useUTC: false,
xAxis: {
type: 'time',
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 40,
},
],
grid: {
bottom: 80,
},
dataZoom: [
{
type: 'inside',
// xAxisIndex: [0],
start: 0,
end: 100,
},
{
// xAxisIndex: [0],
bottom: 10,
start: 0,
end: 100,
...dataZoomPartial,
},
],
};
chart.value?.setOption(chartOptionsLoc, { noMerge: true });
updateChart(true);
}
onMounted(() => {
initializeChart();
});
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,125 +44,113 @@ 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: { dailyStats: {
'v-chart': ECharts, type: Object as () => DailyReturnValue,
required: true,
}, },
props: { showTitle: {
dailyStats: { type: Boolean,
type: Object as () => DailyReturnValue, default: true,
required: true,
},
showTitle: {
type: Boolean,
default: true,
},
}, },
});
setup(props) { const settingsStore = useSettingsStore();
const settingsStore = useSettingsStore(); const absoluteMin = computed(() =>
const absoluteMin = computed(() => props.dailyStats.data.reduce(
props.dailyStats.data.reduce( (min, p) => (p.abs_profit < min ? p.abs_profit : min),
(min, p) => (p.abs_profit < min ? p.abs_profit : min), props.dailyStats.data[0]?.abs_profit,
props.dailyStats.data[0]?.abs_profit, ),
), );
); const absoluteMax = computed(() =>
const absoluteMax = computed(() => props.dailyStats.data.reduce(
props.dailyStats.data.reduce( (max, p) => (p.abs_profit > max ? p.abs_profit : max),
(max, p) => (p.abs_profit > max ? p.abs_profit : max), props.dailyStats.data[0]?.abs_profit,
props.dailyStats.data[0]?.abs_profit, ),
), );
); const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => { return {
return { title: {
title: { text: 'Daily profit',
text: 'Daily profit', show: props.showTitle,
show: props.showTitle, },
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'abs_profit', 'trade_count'],
source: props.dailyStats.data,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
}, },
backgroundColor: 'rgba(0, 0, 0, 0)', },
dataset: { },
dimensions: ['date', 'abs_profit', 'trade_count'], legend: {
source: props.dailyStats.data, data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
}, right: '5%',
tooltip: { },
trigger: 'axis', xAxis: [
axisPointer: { {
type: 'line', type: 'category',
label: { },
backgroundColor: '#6a7985', ],
}, visualMap: [
}, {
}, dimension: 1,
legend: { seriesIndex: 0,
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT], show: false,
right: '5%', pieces: [
},
xAxis: [
{ {
type: 'category', max: 0.0,
min: absoluteMin.value,
color: 'red',
},
{
min: 0.0,
max: absoluteMax.value,
color: 'green',
}, },
], ],
visualMap: [ },
{ ],
dimension: 1, yAxis: [
seriesIndex: 0, {
show: false, type: 'value',
pieces: [ name: CHART_ABS_PROFIT,
{ splitLine: {
max: 0.0, show: false,
min: absoluteMin.value, },
color: 'red', nameRotate: 90,
}, nameLocation: 'middle',
{ nameGap: 40,
min: 0.0, },
max: absoluteMax.value, {
color: 'green', type: 'value',
}, name: CHART_TRADE_COUNT,
], nameRotate: 90,
}, nameLocation: 'middle',
], nameGap: 30,
yAxis: [ },
{ ],
type: 'value', series: [
name: CHART_ABS_PROFIT, {
splitLine: { type: 'line',
show: false, name: CHART_ABS_PROFIT,
}, // Color is induced by visualMap
nameRotate: 90, },
nameLocation: 'middle', {
nameGap: 40, type: 'bar',
}, name: CHART_TRADE_COUNT,
{ itemStyle: {
type: 'value', color: 'rgba(150,150,150,0.3)',
name: CHART_TRADE_COUNT, },
nameRotate: 90, yAxisIndex: 1,
nameLocation: 'middle', },
nameGap: 30, ],
}, };
],
series: [
{
type: 'line',
name: CHART_ABS_PROFIT,
// Color is induced by visualMap
},
{
type: 'bar',
name: CHART_TRADE_COUNT,
itemStyle: {
color: 'rgba(150,150,150,0.3)',
},
yAxisIndex: 1,
},
],
};
});
return {
dailyChartOptions,
settingsStore,
};
},
}); });
</script> </script>

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,121 +45,112 @@ 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', trades: { required: true, type: Array as () => Trade[] },
components: { showTitle: { default: true, type: Boolean },
'v-chart': ECharts, });
}, const settingsStore = useSettingsStore();
props: {
trades: { required: true, type: Array as () => Trade[] },
showTitle: { default: true, type: Boolean },
},
setup(props) {
const settingsStore = useSettingsStore();
const hourlyData = computed(() => { const hourlyData = computed(() => {
const res = new Array(24); const res = new Array(24);
for (let i = 0; i < 24; i += 1) { for (let i = 0; i < 24; i += 1) {
res[i] = { hour: i, hourDesc: `${i}h`, profit: 0.0, count: 0.0 }; res[i] = { hour: i, hourDesc: `${i}h`, profit: 0.0, count: 0.0 };
} }
for (let i = 0, len = props.trades.length; i < len; i += 1) { for (let i = 0, len = props.trades.length; i < len; i += 1) {
const trade = props.trades[i]; const trade = props.trades[i];
if (trade.close_timestamp) { if (trade.close_timestamp) {
const hour = timestampHour(trade.close_timestamp); const hour = timestampHour(trade.close_timestamp);
res[hour].profit += trade.profit_ratio; res[hour].profit += trade.profit_ratio;
res[hour].count += 1; res[hour].count += 1;
} }
} }
return res; return res;
}); });
const hourlyChartOptions = computed((): EChartsOption => { const hourlyChartOptions = computed((): EChartsOption => {
return { return {
title: { title: {
text: 'Hourly Profit', text: 'Hourly Profit',
show: props.showTitle, show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['hourDesc', 'profit', 'count'],
source: hourlyData.value,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
}, },
backgroundColor: 'rgba(0, 0, 0, 0)', },
dataset: { },
dimensions: ['hourDesc', 'profit', 'count'], legend: {
source: hourlyData.value, data: [CHART_PROFIT, CHART_TRADE_COUNT],
right: '5%',
},
xAxis: {
type: 'category',
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
}, },
tooltip: { nameRotate: 90,
trigger: 'axis', nameLocation: 'middle',
axisPointer: { nameGap: 30,
type: 'line', },
label: { {
backgroundColor: '#6a7985', type: 'value',
}, name: CHART_TRADE_COUNT,
}, nameRotate: 90,
}, nameLocation: 'middle',
legend: { nameGap: 30,
data: [CHART_PROFIT, CHART_TRADE_COUNT], },
right: '5%', ],
}, visualMap: [
xAxis: { {
type: 'category', dimension: 1,
}, seriesIndex: 0,
yAxis: [ show: false,
pieces: [
{ {
type: 'value', max: 0.0,
name: CHART_PROFIT, min: -2,
splitLine: { color: 'red',
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 30,
}, },
{ {
type: 'value', min: 0.0,
name: CHART_TRADE_COUNT, max: 2,
nameRotate: 90, color: 'green',
nameLocation: 'middle',
nameGap: 30,
}, },
], ],
visualMap: [ },
{ ],
dimension: 1, series: [
seriesIndex: 0, {
show: false, type: 'line',
pieces: [ name: CHART_PROFIT,
{ animation: false,
max: 0.0, // symbol: 'none',
min: -2, },
color: 'red', {
}, type: 'bar',
{ name: CHART_TRADE_COUNT,
min: 0.0, animation: false,
max: 2, itemStyle: {
color: 'green', color: 'rgba(150,150,150,0.3)',
}, },
], yAxisIndex: 1,
}, },
], ],
series: [ };
{
type: 'line',
name: CHART_PROFIT,
animation: false,
// symbol: 'none',
},
{
type: 'bar',
name: CHART_TRADE_COUNT,
animation: false,
itemStyle: {
color: 'rgba(150,150,150,0.3)',
},
yAxisIndex: 1,
},
],
};
});
return { settingsStore, hourlyChartOptions };
},
}); });
</script> </script>

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"
</b-form-select> :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>
</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 <b-button
v-if="showConfig" class="ms-1 col"
class="ms-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@click="loadConfigFromString"
>Load from String</b-button
>
<b-button
class="ms-1"
variant="primary" variant="primary"
size="sm" size="sm"
data-toggle="tooltip" data-toggle="tooltip"
:disabled="addNewIndicator"
title="Save configuration" title="Save configuration"
@click="savePlotConfig" @click="savePlotConfig"
>Save</b-button >Save</b-button
> >
</div> </div>
<b-button
v-if="showConfig"
class="ms-1 mt-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@click="loadConfigFromString"
>Load from String</b-button
>
<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 <b-form-select
id="indicatorSelector" id="plotTypeSelector"
v-model="selAvailableIndicator" v-model="graphType"
:options="filteredIndicators" size="sm"
:select-size="4" :options="availableGraphTypes"
> >
</b-form-select> </b-form-select>
</b-form-group> </b-form-group>
<b-form-group label="Color" label-for="colsel" size="sm" class="ms-xl-1 col">
<b-input-group>
<b-input-group-prepend>
<b-form-input
v-model="selColor"
type="color"
size="sm"
class="p-0"
style="max-width: 29px"
></b-form-input>
</b-input-group-prepend>
<b-form-input id="colsel" v-model="selColor" size="sm" class="flex-grow-1">
</b-form-input>
<b-input-group-append>
<b-button variant="primary" size="sm" @click="newColor">
<i-mdi-dice-multiple />
</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
</div> </div>
<PlotIndicatorSelect
<b-form-group label="Type" label-for="plotTypeSelector"> v-if="graphType === ChartType.line"
<b-form-select v-model="fillTo"
id="plotTypeSelector" :columns="columns"
v-model="graphType" class="mt-1"
size="sm" label="Select indicator to add"
:options="availableGraphTypes" />
@change="emitIndicator()"
>
</b-form-select>
</b-form-group>
<hr />
<b-form-group label="Color" label-for="colsel" size="sm">
<b-input-group>
<b-input-group-prepend>
<div :style="{ 'background-color': selColor }" class="colorbox me-2"></div>
<!-- <b-form-input
id="colsel"
v-model="selColor"
size="sm"
class="colorbox"
type="color"
:style="{ 'background-color': selColor }"
>
</b-form-input> -->
</b-input-group-prepend>
<b-form-input id="colsel" v-model="selColor" size="sm"> </b-form-input>
<b-input-group-append>
<b-button variant="primary" size="sm" @click="newColor">&#x21bb;</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<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> </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 {};
} }
const val: IndicatorConfig = {
color: selColor.value,
type: graphType.value,
};
if (fillTo.value && graphType.value === ChartType.line) {
val.fill_to = fillTo.value;
}
return { return {
[selAvailableIndicator.value]: { [selAvailableIndicator.value]: val,
color: selColor.value,
type: graphType.value,
},
}; };
}); });
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,92 +59,82 @@ 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', trades: { required: true, type: Array as () => ClosedTrade[] },
components: { showTitle: { default: true, type: Boolean },
'v-chart': ECharts, });
}, const settingsStore = useSettingsStore();
props: { // registerTransform(ecStat.transform.histogram);
trades: { required: true, type: Array as () => ClosedTrade[] }, // console.log(profits);
showTitle: { default: true, type: Boolean }, // const data = [[]];
}, const binOptions = [10, 15, 20, 25, 50];
setup(props) { const data = computed(() => {
const settingsStore = useSettingsStore(); const profits = props.trades.map((trade) => trade.profit_ratio);
// registerTransform(ecStat.transform.histogram);
// console.log(profits);
// const data = [[]];
const binOptions = [10, 15, 20, 25, 50];
const data = computed(() => {
const profits = props.trades.map((trade) => trade.profit_ratio);
return binData(profits, settingsStore.profitDistributionBins); return binData(profits, settingsStore.profitDistributionBins);
}); });
const chartOptions = computed((): EChartsOption => { const chartOptions = computed((): EChartsOption => {
const chartOptionsLoc: EChartsOption = { const chartOptionsLoc: EChartsOption = {
title: { title: {
text: 'Profit distribution', text: 'Profit distribution',
show: props.showTitle, show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
source: data.value,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
}, },
backgroundColor: 'rgba(0, 0, 0, 0)', },
dataset: { },
source: data.value, legend: {
data: [CHART_PROFIT],
right: '5%',
},
xAxis: {
type: 'category',
name: 'Profit %',
nameLocation: 'middle',
nameGap: 25,
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
}, },
tooltip: { nameRotate: 90,
trigger: 'axis', nameLocation: 'middle',
axisPointer: { nameGap: 35,
type: 'line', position: 'left',
label: { },
backgroundColor: '#6a7985', ],
}, // grid: {
}, // bottom: 80,
}, // },
legend: {
data: [CHART_PROFIT],
right: '5%',
},
xAxis: {
type: 'category',
name: 'Profit %',
nameLocation: 'middle',
nameGap: 25,
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 35,
position: 'left',
},
],
// grid: {
// bottom: 80,
// },
series: [ series: [
{ {
type: 'bar', type: 'bar',
name: CHART_PROFIT, name: CHART_PROFIT,
animation: true, animation: true,
encode: { encode: {
x: 'x0', x: 'x0',
y: 'y0', y: 'y0',
}, },
// symbol: 'none', // symbol: 'none',
}, },
], ],
}; };
return chartOptionsLoc; return chartOptionsLoc;
});
// console.log(chartOptions);
return { settingsStore, chartOptions, binOptions };
},
}); });
</script> </script>

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,143 +49,132 @@ 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', trades: { required: true, type: Array as () => ClosedTrade[] },
components: { showTitle: { default: true, type: Boolean },
'v-chart': ECharts, });
}, const settingsStore = useSettingsStore();
props: { const chartData = computed(() => {
trades: { required: true, type: Array as () => ClosedTrade[] }, const res: (number | string)[][] = [];
showTitle: { default: true, type: Boolean }, const sortedTrades = props.trades
}, .slice(0)
setup(props) { .sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
const settingsStore = useSettingsStore(); for (let i = 0, len = sortedTrades.length; i < len; i += 1) {
const chartData = computed(() => { const trade = sortedTrades[i];
const res: (number | string)[][] = []; const entry = [
const sortedTrades = props.trades i,
.slice(0) (trade.profit_ratio * 100).toFixed(2),
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1)); trade.pair,
for (let i = 0, len = sortedTrades.length; i < len; i += 1) { trade.botName,
const trade = sortedTrades[i]; timestampms(trade.close_timestamp),
const entry = [ trade.is_short === undefined || !trade.is_short ? 'Long' : 'Short',
i, ];
(trade.profit_ratio * 100).toFixed(2), res.push(entry);
trade.pair, }
trade.botName, return res;
timestampms(trade.close_timestamp), });
trade.is_short === undefined || !trade.is_short ? 'Long' : 'Short',
];
res.push(entry);
}
return res;
});
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', show: props.showTitle,
show: props.showTitle, },
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'profit'],
source: chartData.value,
},
tooltip: {
trigger: 'axis',
formatter: (params) => {
const botName = params[0].data[3] ? ` | ${params[0].data[3]}` : '';
return `${params[0].data[2]} | ${params[0].data[5]} ${botName}<br>${params[0].data[4]}<br>Profit ${params[0].data[1]} %`;
},
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
}, },
backgroundColor: 'rgba(0, 0, 0, 0)', },
dataset: { },
dimensions: ['date', 'profit'], xAxis: {
source: chartData.value, type: 'value',
}, show: false,
tooltip: { },
trigger: 'axis', yAxis: [
formatter: (params) => { {
const botName = params[0].data[3] ? ` | ${params[0].data[3]}` : ''; type: 'value',
return `${params[0].data[2]} | ${params[0].data[5]} ${botName}<br>${params[0].data[4]}<br>Profit ${params[0].data[1]} %`; name: CHART_PROFIT,
}, splitLine: {
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
xAxis: {
type: 'value',
show: false, show: false,
}, },
yAxis: [ nameRotate: 90,
nameLocation: 'middle',
nameGap: 30,
},
],
grid: {
bottom: 80,
},
dataZoom: [
{
type: 'inside',
start: datazoomStart,
end: 100,
},
{
bottom: 10,
start: datazoomStart,
end: 100,
...dataZoomPartial,
},
],
visualMap: [
{
show: true,
seriesIndex: 0,
pieces: [
{ {
type: 'value', max: 0.0,
name: CHART_PROFIT, color: '#f84960',
splitLine: { },
show: false, {
}, min: 0.0,
nameRotate: 90, color: '#2ed191',
nameLocation: 'middle',
nameGap: 30,
}, },
], ],
grid: { },
bottom: 80, ],
series: [
{
type: 'bar',
name: CHART_PROFIT,
barGap: '0%',
barCategoryGap: '0%',
animation: false,
label: {
show: true,
position: 'top',
rotate: 90,
offset: [7.5, 7.5],
formatter: '{@[1]} %',
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : '#3c3c3c',
},
encode: {
x: 0,
y: 1,
}, },
dataZoom: [
{
type: 'inside',
start: datazoomStart,
end: 100,
},
{
bottom: 10,
start: datazoomStart,
end: 100,
...dataZoomPartial,
},
],
visualMap: [
{
show: true,
seriesIndex: 0,
pieces: [
{
max: 0.0,
color: '#f84960',
},
{
min: 0.0,
color: '#2ed191',
},
],
},
],
series: [
{
type: 'bar',
name: CHART_PROFIT,
barGap: '0%',
barCategoryGap: '0%',
animation: false,
label: {
show: true,
position: 'top',
rotate: 90,
offset: [7.5, 7.5],
formatter: '{@[1]} %',
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : '#3c3c3c',
},
encode: {
x: 0,
y: 1,
},
itemStyle: { itemStyle: {
color: CHART_COLOR, color: CHART_COLOR,
}, },
}, },
], ],
}; };
});
return { settingsStore, chartData, chartOptions };
},
}); });
</script> </script>

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,24 +28,15 @@
</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({ const botStore = useBotStore();
setup() {
const botStore = useBotStore();
onMounted(() => { onMounted(() => {
botStore.activeBot.getBacktestHistory(); botStore.activeBot.getBacktestHistory();
});
return {
timestampms,
botStore,
};
},
}); });
</script> </script>

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({
backtestHistory: {
export default defineComponent({ required: true,
name: 'BacktestResultSelect', type: Object as () => Record<string, StrategyBacktestResult>,
props: {
backtestHistory: {
required: true,
type: Object as () => Record<string, StrategyBacktestResult>,
},
selectedBacktestResultKey: { required: false, default: '', type: String },
},
emits: ['selectionChange'],
setup(_, { emit }) {
const setBacktestResult = (key) => {
emit('selectionChange', key);
};
return {
formatPercent,
setBacktestResult,
};
}, },
selectedBacktestResultKey: { required: false, default: '', type: String },
}); });
const emit = defineEmits(['selectionChange']);
const setBacktestResult = (key) => {
emit('selectionChange', key);
};
</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,86 +52,73 @@
</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({ const botStore = useBotStore();
name: 'BotComparisonList',
components: { ProfitPill },
setup() {
const botStore = useBotStore();
const tableFields: TableField[] = [ const tableFields: TableField[] = [
{ key: 'botName', label: 'Bot' }, { key: 'botName', label: 'Bot' },
{ key: 'trades', label: 'Trades' }, { key: 'trades', label: 'Trades' },
{ key: 'profitOpen', label: 'Open Profit' }, { key: 'profitOpen', label: 'Open Profit' },
{ key: 'profitClosed', label: 'Closed Profit' }, { key: 'profitClosed', label: 'Closed Profit' },
{ key: 'balance', label: 'Balance' }, { key: 'balance', label: 'Balance' },
{ key: 'winVsLoss', label: 'W/L' }, { key: 'winVsLoss', label: 'W/L' },
]; ];
const tableItems = computed<TableItem[]>(() => { const tableItems = computed<TableItem[]>(() => {
const val: ComparisonTableItems[] = []; const val: ComparisonTableItems[] = [];
const summary: ComparisonTableItems = { const summary: ComparisonTableItems = {
botId: undefined, botId: undefined,
botName: 'Summary', botName: 'Summary',
profitClosed: 0, profitClosed: 0,
profitClosedRatio: undefined, profitClosedRatio: undefined,
profitOpen: 0, profitOpen: 0,
profitOpenRatio: undefined, profitOpenRatio: undefined,
stakeCurrency: 'USDT', stakeCurrency: 'USDT',
wins: 0, wins: 0,
losses: 0, losses: 0,
}; };
Object.entries(botStore.allProfit).forEach(([k, v]: [k: string, v: ProfitInterface]) => { Object.entries(botStore.allProfit).forEach(([k, v]: [k: string, v: ProfitInterface]) => {
const allStakes = botStore.allOpenTrades[k].reduce((a, b) => a + b.stake_amount, 0); const allStakes = botStore.allOpenTrades[k].reduce((a, b) => a + b.stake_amount, 0);
const profitOpenRatio = const profitOpenRatio =
botStore.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) / botStore.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) /
allStakes; allStakes;
const profitOpen = botStore.allOpenTrades[k].reduce((a, b) => a + (b.profit_abs ?? 0), 0); const profitOpen = botStore.allOpenTrades[k].reduce((a, b) => a + (b.profit_abs ?? 0), 0);
// TODO: handle one inactive bot ... // TODO: handle one inactive bot ...
val.push({ val.push({
botId: k, botId: k,
botName: botStore.availableBots[k].botName, botName: botStore.availableBots[k].botName,
trades: `${botStore.allOpenTradeCount[k]} / ${ trades: `${botStore.allOpenTradeCount[k]} / ${
botStore.allBotState[k]?.max_open_trades || 'N/A' botStore.allBotState[k]?.max_open_trades || 'N/A'
}`, }`,
profitClosed: v.profit_closed_coin, profitClosed: v.profit_closed_coin,
profitClosedRatio: v.profit_closed_ratio || 0, profitClosedRatio: v.profit_closed_ratio || 0,
stakeCurrency: botStore.allBotState[k]?.stake_currency || '', stakeCurrency: botStore.allBotState[k]?.stake_currency || '',
profitOpenRatio, profitOpenRatio,
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) {
summary.profitClosed += v.profit_closed_coin;
summary.profitOpen += v.profit_all_coin;
summary.wins += v.winning_trades;
summary.losses += v.losing_trades;
// summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
}
});
val.push(summary);
return val as unknown as TableItem[];
}); });
if (v.profit_closed_coin !== undefined) {
return { summary.profitClosed += v.profit_closed_coin;
formatPrice, summary.profitOpen += v.profit_all_coin;
tableFields, summary.wins += v.winning_trades;
tableItems, summary.losses += v.losing_trades;
botStore, // summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
}; }
}, });
val.push(summary);
return val as unknown as TableItem[];
}); });
</script> </script>

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,105 +57,75 @@ 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';
export default defineComponent({ import ForceEntryForm from './ForceEntryForm.vue';
name: 'BotControls',
components: {
ForceEntryForm,
PlayIcon,
StopIcon,
PauseIcon,
ReloadIcon,
ForceExitIcon,
ForceEntryIcon,
MessageBox,
},
setup() {
const botStore = useBotStore();
const forceEnter = ref<boolean>(false);
const msgBox = ref<typeof MessageBox>();
const isRunning = computed((): boolean => { const botStore = useBotStore();
return botStore.activeBot.botState?.state === 'running'; const forceEnter = ref<boolean>(false);
}); const msgBox = ref<typeof MessageBox>();
const handleStopBot = () => { const isRunning = computed((): boolean => {
const msg: MsgBoxObject = { return botStore.activeBot.botState?.state === 'running';
title: 'Stop Bot',
message: 'Stop the bot loop from running?',
accept: () => {
botStore.activeBot.stopBot();
},
};
msgBox.value?.show(msg);
};
const handleStopBuy = () => {
const msg: MsgBoxObject = {
title: 'Stop Buying',
message: 'Freqtrade will continue to handle open trades.',
accept: () => {
botStore.activeBot.stopBuy();
},
};
msgBox.value?.show(msg);
};
const handleReloadConfig = () => {
const msg: MsgBoxObject = {
title: 'Reload',
message: 'Reload configuration (including strategy)?',
accept: () => {
console.log('reload...');
botStore.activeBot.reloadConfig();
},
};
msgBox.value?.show(msg);
};
const handleForceExit = () => {
const msg: MsgBoxObject = {
title: 'ForceExit all',
message: 'Really forceexit ALL trades?',
accept: () => {
const payload: ForceSellPayload = {
tradeid: 'all',
// TODO: support ordertype (?)
};
botStore.activeBot.forceexit(payload);
},
};
msgBox.value?.show(msg);
};
return {
handleStopBot,
handleStopBuy,
handleReloadConfig,
handleForceExit,
forceEnter,
botStore,
isRunning,
msgBox,
};
},
}); });
const handleStopBot = () => {
const msg: MsgBoxObject = {
title: 'Stop Bot',
message: 'Stop the bot loop from running?',
accept: () => {
botStore.activeBot.stopBot();
},
};
msgBox.value?.show(msg);
};
const handleStopBuy = () => {
const msg: MsgBoxObject = {
title: 'Stop Buying',
message: 'Freqtrade will continue to handle open trades.',
accept: () => {
botStore.activeBot.stopBuy();
},
};
msgBox.value?.show(msg);
};
const handleReloadConfig = () => {
const msg: MsgBoxObject = {
title: 'Reload',
message: 'Reload configuration (including strategy)?',
accept: () => {
console.log('reload...');
botStore.activeBot.reloadConfig();
},
};
msgBox.value?.show(msg);
};
const handleForceExit = () => {
const msg: MsgBoxObject = {
title: 'ForceExit all',
message: 'Really forceexit ALL trades?',
accept: () => {
const payload: ForceSellPayload = {
tradeid: 'all',
// TODO: support ordertype (?)
};
botStore.activeBot.forceexit(payload);
},
};
msgBox.value?.show(msg);
};
</script> </script>

View File

@ -11,32 +11,23 @@
</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({ const botStore = useBotStore();
name: 'BotPerformance', const tableFields = computed<TableField[]>(() => {
setup() { return [
const botStore = useBotStore(); { key: 'pair', label: 'Pair' },
const tableFields = computed<TableField[]>(() => { { key: 'profit', label: 'Profit %' },
return [ {
{ key: 'pair', label: 'Pair' }, key: 'profit_abs',
{ key: 'profit', label: 'Profit %' }, label: `Profit ${botStore.activeBot.botState?.stake_currency}`,
{ formatter: (v: unknown) => formatPrice(v as number, 5),
key: 'profit_abs', },
label: `Profit ${botStore.activeBot.botState?.stake_currency}`, { key: 'count', label: 'Count' },
formatter: (v: unknown) => formatPrice(v as number, 5), ];
},
{ 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({ const botStore = useBotStore();
name: 'BotStatus',
components: { DateTimeTZ },
setup() {
const botStore = useBotStore();
return {
formatPercent,
formatPriceCurrency,
botStore,
};
},
});
</script> </script>

View File

@ -33,70 +33,36 @@
</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', trades: { required: true, type: Array as () => Trade[] },
components: { title: { default: 'Trades', type: String },
CustomTradeListEntry, stakeCurrency: { required: false, default: '', type: String },
}, activeTrades: { default: false, type: Boolean },
props: { showFilter: { default: false, type: Boolean },
trades: { required: true, type: Array as () => Trade[] }, multiBotView: { default: false, type: Boolean },
title: { default: 'Trades', type: String }, emptyText: { default: 'No Trades to show.', type: String },
stakeCurrency: { required: false, default: '', type: String }, stakeCurrencyDecimals: { default: 3, type: Number },
activeTrades: { default: false, type: Boolean },
showFilter: { default: false, type: Boolean },
multiBotView: { default: false, type: Boolean },
emptyText: { default: 'No Trades to show.', type: String },
stakeCurrencyDecimals: { default: 3, type: Number },
},
setup(props) {
const botStore = useBotStore();
const currentPage = ref(1);
const filterText = ref('');
const perPage = props.activeTrades ? 200 : 25;
const rows = computed(() => props.trades.length);
const filteredTrades = computed(() => {
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) => {
botStore.activeBot.setDetailTrade(trade);
};
return {
currentPage,
filterText,
perPage,
filteredTrades,
formatPriceWithDecimals,
handleContextMenuEvent,
tradeClick,
botStore,
rows,
};
},
}); });
const botStore = useBotStore();
const currentPage = ref(1);
const filterText = ref('');
const perPage = props.activeTrades ? 200 : 25;
const rows = computed(() => props.trades.length);
const filteredTrades = computed(() => {
return props.trades.slice((currentPage.value - 1) * perPage, currentPage.value * perPage);
});
const tradeClick = (trade) => {
botStore.activeBot.setDetailTrade(trade);
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -15,39 +15,23 @@
</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: { trade: {
DateTimeTZ, type: Object as () => Trade,
TradeProfit, required: true,
}, },
props: { stakeCurrencyDecimals: {
trade: { type: Number,
type: Object as () => Trade, required: true,
required: true,
},
stakeCurrencyDecimals: {
type: Number,
required: true,
},
showDetails: {
type: Boolean,
default: false,
},
}, },
setup() { showDetails: {
return { type: Boolean,
formatPrice, default: false,
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,51 +19,38 @@
</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({ const botStore = useBotStore();
name: 'DailyStats', const dailyFields = computed<TableField[]>(() => {
components: { const res: TableField[] = [
DailyChart, { key: 'date', label: 'Day' },
}, {
setup() { key: 'abs_profit',
const botStore = useBotStore(); label: 'Profit',
const dailyFields = computed<TableField[]>(() => { // formatter: (value: unknown) => formatPrice(value as number),
const res: TableField[] = [ },
{ key: 'date', label: 'Day' }, {
{ key: 'fiat_value',
key: 'abs_profit', label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`,
label: 'Profit', // formatter: (value: unknown) => formatPrice(value as number, 2),
// formatter: (value: unknown) => formatPrice(value as number), },
}, { key: 'trade_count', label: 'Trades' },
{ ];
key: 'fiat_value', if (botStore.activeBot.botApiVersion >= 2.16)
label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`, res.push({
// formatter: (value: unknown) => formatPrice(value as number, 2), key: 'rel_profit',
}, label: 'Profit%',
{ key: 'trade_count', label: 'Trades' }, formatter: (value: unknown) => formatPercent(value as number, 2),
];
if (botStore.activeBot.botApiVersion >= 2.16)
res.push({
key: 'rel_profit',
label: 'Profit%',
formatter: (value: unknown) => formatPercent(value as number, 2),
});
return res;
}); });
onMounted(() => { return res;
botStore.activeBot.getDaily(); });
}); onMounted(() => {
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,89 +91,73 @@
</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({ const newblacklistpair = ref('');
name: 'FTBotAPIPairList', const blackListShow = ref(false);
components: { DeleteIcon }, const blacklistSelect = ref<number[]>([]);
setup() { const botStore = useBotStore();
const newblacklistpair = ref('');
const blackListShow = ref(false);
const blacklistSelect = ref<number[]>([]);
const botStore = useBotStore();
const initBlacklist = () => { const initBlacklist = () => {
if (botStore.activeBot.whitelist.length === 0) { if (botStore.activeBot.whitelist.length === 0) {
botStore.activeBot.getWhitelist(); botStore.activeBot.getWhitelist();
} }
if (botStore.activeBot.blacklist.length === 0) { if (botStore.activeBot.blacklist.length === 0) {
botStore.activeBot.getBlacklist(); botStore.activeBot.getBlacklist();
} }
}; };
const addBlacklistPair = () => { const addBlacklistPair = () => {
if (newblacklistpair.value) { if (newblacklistpair.value) {
blackListShow.value = false; blackListShow.value = false;
botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] }); botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] });
newblacklistpair.value = ''; newblacklistpair.value = '';
} }
}; };
const blacklistSelectClick = (key) => { const blacklistSelectClick = (key) => {
console.log(key); console.log(key);
const index = blacklistSelect.value.indexOf(key); const index = blacklistSelect.value.indexOf(key);
if (index > -1) { if (index > -1) {
blacklistSelect.value.splice(index, 1); blacklistSelect.value.splice(index, 1);
} else { } else {
blacklistSelect.value.push(key); blacklistSelect.value.push(key);
} }
}; };
const deletePairs = () => { const deletePairs = () => {
if (blacklistSelect.value.length === 0) { if (blacklistSelect.value.length === 0) {
console.log('nothing to delete'); console.log('nothing to delete');
return; return;
} }
// const pairlist = blacklistSelect.value; // const pairlist = blacklistSelect.value;
const pairlist = botStore.activeBot.blacklist.filter( const pairlist = botStore.activeBot.blacklist.filter(
(value, index) => blacklistSelect.value.indexOf(index) > -1, (value, index) => blacklistSelect.value.indexOf(index) > -1,
); );
console.log('Deleting pairs: ', pairlist); console.log('Deleting pairs: ', pairlist);
botStore.activeBot.deleteBlacklist(pairlist); botStore.activeBot.deleteBlacklist(pairlist);
blacklistSelect.value = []; blacklistSelect.value = [];
}; };
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,37 +1,29 @@
<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({ const botStore = useBotStore();
name: 'LogViewer',
setup() {
const botStore = useBotStore();
onMounted(async () => { onMounted(async () => {
botStore.activeBot.getLogs(); botStore.activeBot.getLogs();
}); });
const formattedLogs = computed(() => { const formattedLogs = computed(() => {
let result = ''; let result = '';
for (let i = 0, len = botStore.activeBot.lastLogs.length; i < len; i += 1) { for (let i = 0, len = botStore.activeBot.lastLogs.length; i < len; i += 1) {
const log = botStore.activeBot.lastLogs[i]; const log = botStore.activeBot.lastLogs[i];
result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`; result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`;
} }
return result; return result;
});
return {
botStore,
formattedLogs,
};
},
}); });
</script> </script>

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,45 +23,30 @@
</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';
const botStore = useBotStore();
export default defineComponent({ const tableFields: TableField[] = [
name: 'PairLockList', { key: 'pair', label: 'Pair' },
components: { DeleteIcon }, { key: 'lock_end_timestamp', label: 'Until', formatter: (value) => timestampms(value as number) },
setup() { { key: 'reason', label: 'Reason' },
const botStore = useBotStore(); { key: 'actions' },
];
const tableFields = [ const removePairLock = (item: Lock) => {
{ key: 'pair', label: 'Pair' }, console.log(item);
{ key: 'lock_end_timestamp', label: 'Until', formatter: 'timestampms' }, if (item.id !== undefined) {
{ key: 'reason', label: 'Reason' }, botStore.activeBot.deleteLock(item.id);
{ key: 'actions' }, } else {
]; showAlert('This Freqtrade version does not support deleting locks.');
}
const removePairLock = (item: Lock) => { };
console.log(item);
if (item.id !== undefined) {
botStore.activeBot.deleteLock(item.id);
} else {
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,34 +12,22 @@
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({ const botStore = useBotStore();
name: 'ReloadControl', const autoRefreshLoc = computed({
components: { RefreshIcon }, get() {
setup() { return botStore.globalAutoRefresh;
const botStore = useBotStore(); },
const autoRefreshLoc = computed({ set(newValue: boolean) {
get() { botStore.setGlobalAutoRefresh(newValue);
return botStore.globalAutoRefresh;
},
set(newValue: boolean) {
botStore.setGlobalAutoRefresh(newValue);
},
});
return {
botStore,
autoRefreshLoc,
};
}, },
}); });
</script> </script>

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,62 +7,50 @@
></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', value: { default: '', type: String },
props: { belowTimeframe: { required: false, default: '', type: String },
value: { default: '', type: String },
belowTimeframe: { required: false, default: '', type: String },
},
emits: ['input'],
setup(props, { emit }) {
const selectedTimeframe = ref('');
// The below list must always remain sorted correctly!
const availableTimeframesBase = [
// Placeholder value
{ value: '', text: 'Use strategy default' },
'1m',
'3m',
'5m',
'15m',
'30m',
'1h',
'2h',
'4h',
'6h',
'8h',
'12h',
'1d',
'3d',
'1w',
'2w',
'1M',
'1y',
];
const availableTimeframes = computed(() => {
if (!props.belowTimeframe) {
return availableTimeframesBase;
}
const idx = availableTimeframesBase.findIndex((v) => v === props.belowTimeframe);
return [...availableTimeframesBase].splice(0, idx);
});
const emitSelectedTimeframe = () => {
emit('input', selectedTimeframe.value);
};
return {
availableTimeframesBase,
availableTimeframes,
emitSelectedTimeframe,
selectedTimeframe,
};
},
}); });
const emit = defineEmits(['input']);
const selectedTimeframe = ref('');
// The below list must always remain sorted correctly!
const availableTimeframesBase = [
// Placeholder value
{ value: '', text: 'Use strategy default' },
'1m',
'3m',
'5m',
'15m',
'30m',
'1h',
'2h',
'4h',
'6h',
'8h',
'12h',
'1d',
'3d',
'1w',
'2w',
'1M',
'1y',
];
const availableTimeframes = computed(() => {
if (!props.belowTimeframe) {
return availableTimeframesBase;
}
const idx = availableTimeframesBase.findIndex((v) => v === props.belowTimeframe);
return [...availableTimeframesBase].splice(0, idx);
});
const emitSelectedTimeframe = () => {
emit('input', selectedTimeframe.value);
};
</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({
trade: { required: true, type: Object as () => Trade },
export default defineComponent({ stakeCurrency: { required: true, type: String },
name: 'TradeDetail',
components: { ValuePair, TradeProfit, DateTimeTZ },
props: {
trade: { required: true, type: Object as () => Trade },
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,40 +2,34 @@
<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', date: { required: true, type: Number },
props: { showTimezone: { required: false, type: Boolean, default: false },
date: { required: true, type: Number }, dateOnly: { required: false, type: Boolean, default: false },
showTimezone: { required: false, type: Boolean, default: false }, });
dateOnly: { required: false, type: Boolean, default: false }, const formattedDate = computed((): string => {
}, if (props.dateOnly) {
setup(props) { return timestampToDateString(props.date);
const formattedDate = computed((): string => { }
if (props.dateOnly) { if (props.showTimezone) {
return timestampToDateString(props.date); return timestampmsWithTimezone(props.date);
} }
if (props.showTimezone) { return timestampms(props.date);
return timestampmsWithTimezone(props.date); });
}
return timestampms(props.date);
});
const timezoneTooltip = computed((): string => { const timezoneTooltip = computed((): string => {
const time1 = timestampmsWithTimezone(props.date); const time1 = timestampmsWithTimezone(props.date);
const timeUTC = timestampmsWithTimezone(props.date, 'UTC'); const timeUTC = timestampmsWithTimezone(props.date, 'UTC');
if (time1 === timeUTC) { if (time1 === timeUTC) {
return timeUTC; return timeUTC;
} }
return `${time1}\n${timeUTC}`; return `${time1}\n${timeUTC}`;
});
return { formattedDate, timezoneTooltip };
},
}); });
</script> </script>

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', () => {
const msg = 'Test description'; it('Renders a message', () => {
cy.mount(ValuePair, { props: { description: msg } }); const msg = 'Test description';
// https://github.com/cypress-io/cypress/issues/26628
cy.get('label').contains(msg); // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
cy.mount(ValuePair, { props: { description: 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({
description: { type: String, required: true },
export default defineComponent({ help: { type: String, default: '', required: false },
name: 'ValuePair', classLabel: { type: String, default: 'col-4 fw-bold mb-0' },
components: { InfoBox }, classValue: { type: String, default: 'col-8' },
props: {
description: { type: String, required: true },
help: { type: String, default: '', required: false },
classLabel: { type: String, default: 'col-4 fw-bold mb-0' },
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({ const botStore = useBotStore();
name: 'NavFooter',
components: { OpenTradesIcon, ClosedTradesIcon, BalanceIcon, PairListIcon, DashboardIcon },
setup() {
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)
response[k] = { .sort((a, b) => (allInfo[a].sortId ?? 0) - (allInfo[b].sortId ?? 0))
botId: k, .forEach((k, idx) => {
botName: v.botName, response[k] = {
botUrl: v.apiUrl, botId: k,
sortId: v.sortId ?? idx, botName: allInfo[k].botName,
}; botUrl: allInfo[k].apiUrl,
}); 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);
setPlotConfig(plotConfig: PlotConfig) { if (this.isEditing) {
console.log('emit...'); this.editablePlotConfig = deepClone(this.customPlotConfigs[this.plotConfigName]);
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', history: { default: false, type: Boolean },
components: {
CustomTradeList,
TradeDetail,
BackIcon,
},
props: {
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