mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-26 21:15:15 +00:00
Merge branch 'main' into feat/pairlistconfig
This commit is contained in:
commit
36737ae6dc
|
@ -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 \
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -22,3 +22,6 @@ yarn-error.log*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
|
||||||
|
components.d.ts
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
12045
cypress/fixtures/pair_candles_btc_1m.json
Normal file
12045
cypress/fixtures/pair_candles_btc_1m.json
Normal file
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
src/App.vue
31
src/App.vue
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
@ -15,7 +15,9 @@
|
||||||
>
|
>
|
||||||
</v-select>
|
</v-select>
|
||||||
|
|
||||||
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">↻</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">
|
||||||
⚙
|
<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({
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
38
src/components/charts/PlotConfigSelect.vue
Normal file
38
src/components/charts/PlotConfigSelect.vue
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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">↻</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>
|
|
||||||
|
|
48
src/components/charts/PlotIndicatorSelect.vue
Normal file
48
src/components/charts/PlotIndicatorSelect.vue
Normal 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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
aria-label="Refresh"
|
aria-label="Refresh"
|
||||||
@click="botStore.activeBot.getBacktestHistory"
|
@click="botStore.activeBot.getBacktestHistory"
|
||||||
>
|
>
|
||||||
↻
|
<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>
|
||||||
|
|
||||||
|
|
|
@ -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[] },
|
||||||
|
|
49
src/components/ftbot/BacktestResultPeriodBreakdown.vue
Normal file
49
src/components/ftbot/BacktestResultPeriodBreakdown.vue
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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">
|
||||||
>↻</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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">↻</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>
|
||||||
|
|
|
@ -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">✔</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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
>
|
>
|
||||||
</b-form-select>
|
</b-form-select>
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
<b-button @click="botStore.activeBot.getFreqAIModelList">↻</b-button>
|
<b-button @click="botStore.activeBot.getFreqAIModelList">
|
||||||
|
<i-mdi-refresh />
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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">↻</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>
|
||||||
|
|
||||||
|
|
|
@ -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">↻</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>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{{ comb.pair }}
|
{{ comb.pair }}
|
||||||
<span v-if="comb.locks" :title="comb.lockReason"> 🔒 </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" />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
>
|
>
|
||||||
</b-form-select>
|
</b-form-select>
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
<b-button @click="botStore.activeBot.getStrategyList">↻</b-button>
|
<b-button @click="botStore.activeBot.getStrategyList">
|
||||||
|
<i-mdi-refresh />
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
118
src/components/general/EditValue.vue
Normal file
118
src/components/general/EditValue.vue
Normal 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>
|
|
@ -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 },
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 };
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
50
src/shared/charts/areaPlotDataset.ts
Normal file
50
src/shared/charts/areaPlotDataset.ts
Normal 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;
|
||||||
|
}
|
69
src/shared/charts/candleChartSeries.ts
Normal file
69
src/shared/charts/candleChartSeries.ts
Normal 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;
|
||||||
|
}
|
|
@ -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
3
src/shared/deepClone.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function deepClone<T>(object: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
}
|
|
@ -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]);
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -8,7 +8,3 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"types": [
|
"types": [
|
||||||
"cypress",
|
"cypress",
|
||||||
"vite/client",
|
"vite/client",
|
||||||
|
"unplugin-icons/types/vue",
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user