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 \
|
||||
&& sudo apt-get install -y vim \
|
||||
|
|
|
@ -1,35 +1,41 @@
|
|||
/* cSpell:disable */
|
||||
{
|
||||
"name": "frequi",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"forwardPorts": [
|
||||
3000
|
||||
],
|
||||
"mounts": [
|
||||
"source=frequi-bashhistory,target=/home/node/commandhistory,type=volume"
|
||||
],
|
||||
"remoteUser": "node",
|
||||
"settings": {
|
||||
// "editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"emmet.includeLanguages": {
|
||||
"vue": "html",
|
||||
"vue-html": "html"
|
||||
},
|
||||
"workbench.iconTheme": "vscode-icons",
|
||||
},
|
||||
"extensions": [
|
||||
"vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"marquesmps.dockerfile-validator",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"hediet.vscode-drawio",
|
||||
],
|
||||
"postCreateCommand": "yarn install",
|
||||
"name": "frequi",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"forwardPorts": [
|
||||
3000
|
||||
],
|
||||
"mounts": [
|
||||
"source=frequi-bashhistory,target=/home/node/commandhistory,type=volume"
|
||||
],
|
||||
"remoteUser": "node",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
// "editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"emmet.includeLanguages": {
|
||||
"vue": "html",
|
||||
"vue-html": "html"
|
||||
},
|
||||
"workbench.iconTheme": "vscode-icons"
|
||||
},
|
||||
"extensions": [
|
||||
"vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"yzhang.markdown-all-in-one",
|
||||
"marquesmps.dockerfile-validator",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"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:
|
||||
matrix:
|
||||
os: [ ubuntu-22.04 ]
|
||||
node: [ "16", "18", "19"]
|
||||
node: [ "16", "18", "19", "20"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -22,3 +22,6 @@ yarn-error.log*
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.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
|
||||
|
||||
|
|
|
@ -24,4 +24,8 @@ export function defaultMocks() {
|
|||
cy.intercept('GET', '**/api/v1/show_config', {
|
||||
fixture: 'show_config.json',
|
||||
}).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": {
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@vuepic/vue-datepicker": "^4.4.0",
|
||||
"@vueuse/core": "^10.0.2",
|
||||
"@vueuse/integrations": "^10.0.2",
|
||||
"axios": "^1.3.5",
|
||||
"@vuepic/vue-datepicker": "^5.1.1",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/integrations": "^10.1.2",
|
||||
"axios": "^1.4.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"bootstrap-vue-next": "^0.8.5",
|
||||
"core-js": "^3.30.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"bootstrap-vue-next": "^0.8.11",
|
||||
"core-js": "^3.30.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"echarts": "^5.4.2",
|
||||
"favico.js": "^0.3.10",
|
||||
"humanize-duration": "^3.28.0",
|
||||
"pinia": "^2.0.34",
|
||||
"pinia": "^2.0.36",
|
||||
"pinia-plugin-persistedstate": "^3.1.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vue": "^3.2.47",
|
||||
"vue": "^3.3.2",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-demi": "^0.14.0",
|
||||
"vue-echarts": "^6.5.4",
|
||||
"vue-material-design-icons": "^5.2.0",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-demi": "^0.14.1",
|
||||
"vue-echarts": "^6.5.5",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-select": "^4.0.0-beta.6",
|
||||
"vue3-drr-grid-layout": "^1.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/vite-dev-server": "^5.0.5",
|
||||
"@cypress/vue": "^5.0.5",
|
||||
"@iconify-json/mdi": "^1.1.52",
|
||||
"@types/echarts": "^4.9.17",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||
"@typescript-eslint/parser": "^5.59.6",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/compiler-sfc": "3.3.2",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/runtime-dom": "^3.2.47",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/runtime-dom": "^3.3.2",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"cypress": "^12.10.0",
|
||||
"eslint": "^8.38.0",
|
||||
"cypress": "^12.12.0",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.11.0",
|
||||
"eslint-plugin-vue": "^9.13.0",
|
||||
"mutationobserver-shim": "^0.3.7",
|
||||
"portal-vue": "^3.0.0",
|
||||
"prettier": "^2.8.7",
|
||||
"sass": "^1.62.0",
|
||||
"prettier": "^2.8.8",
|
||||
"sass": "^1.62.1",
|
||||
"typescript": "~5.0.4",
|
||||
"vite": "^4.2.2",
|
||||
"vitest": "^0.30.1",
|
||||
"vue-tsc": "^1.2.0"
|
||||
"unplugin-icons": "^0.16.1",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import NavBar from '@/components/layout/NavBar.vue';
|
||||
import NavFooter from '@/components/layout/NavFooter.vue';
|
||||
import BodyLayout from '@/components/layout/BodyLayout.vue';
|
||||
import { setTimezone } from './shared/formatters';
|
||||
import { defineComponent, onMounted, watch } from 'vue';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { useSettingsStore } from './stores/settings';
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: { NavBar, BodyLayout, NavFooter },
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore();
|
||||
onMounted(() => {
|
||||
setTimezone(settingsStore.timezone);
|
||||
});
|
||||
watch(
|
||||
() => settingsStore.timezone,
|
||||
(tz) => {
|
||||
console.log('timezone changed', tz);
|
||||
setTimezone(tz);
|
||||
},
|
||||
);
|
||||
return {};
|
||||
},
|
||||
const settingsStore = useSettingsStore();
|
||||
onMounted(() => {
|
||||
setTimezone(settingsStore.timezone);
|
||||
});
|
||||
watch(
|
||||
() => settingsStore.timezone,
|
||||
(tz) => {
|
||||
console.log('timezone changed', tz);
|
||||
setTimezone(tz);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -11,18 +11,18 @@
|
|||
switch
|
||||
@change="changeEvent"
|
||||
>
|
||||
<OnlineIcon
|
||||
<div
|
||||
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'"
|
||||
></OnlineIcon>
|
||||
<LoggedOutIcon
|
||||
v-else
|
||||
class="offline"
|
||||
title="Login info expired, please login again."
|
||||
></LoggedOutIcon>
|
||||
>
|
||||
<i-mdi-circle
|
||||
class="ms-2 me-1 align-middle"
|
||||
:class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else title="Login info expired, please login again.">
|
||||
<i-mdi-cancel class="offline" />
|
||||
</div>
|
||||
</b-form-checkbox>
|
||||
<div v-if="!noButtons" class="float-end d-flex flex-align-center">
|
||||
<b-button
|
||||
|
@ -32,13 +32,13 @@
|
|||
title="Edit bot"
|
||||
@click="$emit('edit')"
|
||||
>
|
||||
<EditIcon :size="16" />
|
||||
<i-mdi-pencil />
|
||||
</b-button>
|
||||
<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 class="ms-1" size="sm" title="Delete bot" @click="botRemoveModalVisible = true">
|
||||
<DeleteIcon :size="16" title="Delete Bot" />
|
||||
<i-mdi-delete />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,59 +54,34 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script 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';
|
||||
<script setup lang="ts">
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { BotDescriptor } from '@/types';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BotEntry',
|
||||
components: {
|
||||
DeleteIcon,
|
||||
EditIcon,
|
||||
LoginIcon,
|
||||
OnlineIcon,
|
||||
LoggedOutIcon,
|
||||
const props = defineProps({
|
||||
bot: { required: true, type: Object as () => BotDescriptor },
|
||||
noButtons: { default: false, type: Boolean },
|
||||
});
|
||||
defineEmits(['edit', 'editLogin']);
|
||||
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;
|
||||
},
|
||||
props: {
|
||||
bot: { required: true, type: Object as () => BotDescriptor },
|
||||
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,
|
||||
};
|
||||
set() {
|
||||
// pass
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
class="d-flex"
|
||||
@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
|
||||
v-if="editingBots.includes(bot.botId)"
|
||||
:bot="bot"
|
||||
|
@ -35,15 +35,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoginModal from '@/views/LoginModal.vue';
|
||||
import BotEntry from '@/components/BotEntry.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 { AuthStorageWithBotId, BotDescriptor } from '@/types';
|
||||
import { useSortable } from '@vueuse/integrations/useSortable';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
defineProps({
|
||||
small: { default: false, type: Boolean },
|
||||
|
|
|
@ -23,6 +23,14 @@
|
|||
:state="urlState === '' ? null : urlState"
|
||||
@keydown.enter="handleOk"
|
||||
></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
|
||||
:state="nameState"
|
||||
|
@ -78,7 +86,7 @@
|
|||
import { useUserService } from '@/shared/userService';
|
||||
import { AuthPayload, AuthStorageWithBotId } from '@/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
|
@ -113,10 +121,16 @@ const emitLoginResult = (value: boolean) => {
|
|||
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 valid = formRef.value?.checkValidity();
|
||||
nameState.value = valid || auth.value.username !== '';
|
||||
pwdState.value = valid || auth.value.password !== '';
|
||||
urlState.value = valid || auth.value.url !== '';
|
||||
return valid;
|
||||
};
|
||||
|
||||
|
|
|
@ -11,49 +11,34 @@
|
|||
|
||||
<div class="d-flex ms-2">
|
||||
<b-button type="submit" size="sm" title="Save">
|
||||
<CheckIcon :size="16" />
|
||||
<i-mdi-check />
|
||||
</b-button>
|
||||
|
||||
<b-button class="ms-1" size="sm" title="Cancel" @click="$emit('cancelled')">
|
||||
<CloseIcon :size="16" />
|
||||
<i-mdi-close />
|
||||
</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script 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';
|
||||
<script setup lang="ts">
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { BotDescriptor } from '@/types';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BotRename',
|
||||
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 props = defineProps({
|
||||
bot: { type: Object as () => BotDescriptor, required: true },
|
||||
});
|
||||
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>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<b-link variant="outline-primary" class="nav-link" @click="toggleNight">
|
||||
<ThemeLightDark :size="16" />
|
||||
<i-mdi-brightness-6 />
|
||||
</b-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import ThemeLightDark from 'vue-material-design-icons/Brightness6.vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const activeTheme = ref('');
|
||||
const settingsStore = useSettingsStore();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-chart
|
||||
<e-charts
|
||||
v-if="currencies"
|
||||
:option="balanceChartOptions"
|
||||
:theme="settingsStore.chartTheme"
|
||||
|
@ -7,7 +7,7 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -22,9 +22,9 @@ import {
|
|||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
|
||||
import { BalanceRecords } from '@/types';
|
||||
import { BalanceValues } from '@/types';
|
||||
import { formatPriceCurrency } from '@/shared/formatters';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
use([
|
||||
|
@ -37,67 +37,57 @@ use([
|
|||
LabelLayout,
|
||||
]);
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BalanceChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
props: {
|
||||
currencies: { required: true, type: Array as () => BalanceRecords[] },
|
||||
showTitle: { required: false, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const props = defineProps({
|
||||
currencies: { required: true, type: Array as () => BalanceValues[] },
|
||||
showTitle: { required: false, type: Boolean },
|
||||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const balanceChartOptions = computed((): EChartsOption => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Balance',
|
||||
show: props.showTitle,
|
||||
const balanceChartOptions = computed((): EChartsOption => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Balance',
|
||||
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%'],
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['balance', 'currency', 'est_stake', 'free', 'used', 'stake'],
|
||||
source: props.currencies,
|
||||
label: {
|
||||
formatter: '{b} - {d}%',
|
||||
},
|
||||
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)})`;
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
// 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>
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,7 +15,9 @@
|
|||
>
|
||||
</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"
|
||||
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
|
||||
>
|
||||
|
@ -35,18 +37,12 @@
|
|||
>
|
||||
|
||||
<div class="ms-2">
|
||||
<b-form-select
|
||||
v-model="plotStore.plotConfigName"
|
||||
:options="plotStore.availablePlotConfigNames"
|
||||
size="sm"
|
||||
@change="plotStore.plotConfigChanged"
|
||||
>
|
||||
</b-form-select>
|
||||
<plot-config-select></plot-config-select>
|
||||
</div>
|
||||
|
||||
<div class="ms-2 me-0 me-md-1">
|
||||
<b-button size="sm" title="Plot configurator" @click="showConfigurator">
|
||||
⚙
|
||||
<i-mdi-cog width="12" height="12" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -91,15 +87,16 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Trade, PairHistory, LoadingStatus, ChartSliderPosition } from '@/types';
|
||||
import CandleChart from '@/components/charts/CandleChart.vue';
|
||||
import PlotConfigSelect from '@/components/charts/PlotConfigSelect.vue';
|
||||
import PlotConfigurator from '@/components/charts/PlotConfigurator.vue';
|
||||
import vSelect from 'vue-select';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
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 { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
trades: { required: false, default: () => [], type: Array as () => Trade[] },
|
||||
|
@ -111,6 +108,7 @@ const props = defineProps({
|
|||
timerange: { required: false, default: '', type: String },
|
||||
/** Only required if historicView is true */
|
||||
strategy: { required: false, default: '', type: String },
|
||||
freqaiModel: { required: false, default: undefined, type: String },
|
||||
sliderPosition: {
|
||||
required: false,
|
||||
type: Object as () => ChartSliderPosition,
|
||||
|
@ -176,6 +174,7 @@ const refresh = () => {
|
|||
timeframe: props.timeframe,
|
||||
timerange: props.timerange,
|
||||
strategy: props.strategy,
|
||||
freqaimodel: props.freqaiModel,
|
||||
});
|
||||
} else {
|
||||
botStore.activeBot.getPairCandles({
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -17,10 +17,20 @@ import {
|
|||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
|
||||
import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types';
|
||||
import { defineComponent, computed, ComputedRef } from 'vue';
|
||||
import {
|
||||
ClosedTrade,
|
||||
CumProfitData,
|
||||
CumProfitDataPerDate,
|
||||
CumProfitChartData,
|
||||
Trade,
|
||||
} from '@/types';
|
||||
import { computed } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { dataZoomPartial } from '@/shared/charts/chartZoom';
|
||||
import { ref } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import { watchThrottled } from '@vueuse/core';
|
||||
|
||||
use([
|
||||
BarChart,
|
||||
|
@ -38,158 +48,226 @@ use([
|
|||
// Define Column labels here to avoid typos
|
||||
const CHART_PROFIT = 'Profit';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CumProfitChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
profitColumn: { default: 'profit_abs', type: String },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
// const botList = ref<string[]>([]);
|
||||
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
|
||||
const props = defineProps({
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
openTrades: { required: true, type: Array as () => Trade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
profitColumn: { default: 'profit_abs', type: String },
|
||||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
// const botList = ref<string[]>([]);
|
||||
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
|
||||
|
||||
const cumulativeData: ComputedRef<{ date: number; profit: number }[]> = computed(() => {
|
||||
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;
|
||||
const chart = ref<typeof ECharts>();
|
||||
|
||||
for (let i = 0, len = closedTrades.length; i < len; i += 1) {
|
||||
const trade = closedTrades[i];
|
||||
const openProfit = computed<number>(() => {
|
||||
return props.openTrades.reduce((a, v) => a + v[props.profitColumn], 0);
|
||||
});
|
||||
|
||||
if (trade.close_timestamp && trade[props.profitColumn]) {
|
||||
profit += trade[props.profitColumn];
|
||||
if (!resD[trade.close_timestamp]) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
|
||||
const cumulativeData = computed<CumProfitChartData[]>(() => {
|
||||
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 trade = closedTrades[i];
|
||||
|
||||
if (trade.close_timestamp && trade[props.profitColumn]) {
|
||||
profit += trade[props.profitColumn];
|
||||
if (!resD[trade.close_timestamp]) {
|
||||
// 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 obj = { date: parseInt(k, 10), profit: v.profit };
|
||||
// TODO: The below could allow "lines" per bot"
|
||||
// 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',
|
||||
// });
|
||||
const valueArray: CumProfitChartData[] = Object.entries(resD).map(
|
||||
([k, v]: [string, CumProfitData]) => {
|
||||
const obj = { date: parseInt(k, 10), profit: v.profit };
|
||||
// TODO: The below could allow "lines" per bot"
|
||||
// this.botList.forEach((botId) => {
|
||||
// obj[botId] = v[botId];
|
||||
// });
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-chart
|
||||
<e-charts
|
||||
v-if="dailyStats.data"
|
||||
:option="dailyChartOptions"
|
||||
:theme="settingsStore.chartTheme"
|
||||
|
@ -7,8 +7,8 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ComputedRef } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import ECharts from 'vue-echarts';
|
||||
// import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -44,125 +44,113 @@ use([
|
|||
const CHART_ABS_PROFIT = 'Absolute profit';
|
||||
const CHART_TRADE_COUNT = 'Trade Count';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
const props = defineProps({
|
||||
dailyStats: {
|
||||
type: Object as () => DailyReturnValue,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
dailyStats: {
|
||||
type: Object as () => DailyReturnValue,
|
||||
required: true,
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const absoluteMin = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
const absoluteMax = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Daily profit',
|
||||
show: props.showTitle,
|
||||
const settingsStore = useSettingsStore();
|
||||
const absoluteMin = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
const absoluteMax = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Daily profit',
|
||||
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'],
|
||||
source: props.dailyStats.data,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: [
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
},
|
||||
],
|
||||
visualMap: [
|
||||
{
|
||||
dimension: 1,
|
||||
seriesIndex: 0,
|
||||
show: false,
|
||||
pieces: [
|
||||
{
|
||||
type: 'category',
|
||||
max: 0.0,
|
||||
min: absoluteMin.value,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
max: absoluteMax.value,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
visualMap: [
|
||||
{
|
||||
dimension: 1,
|
||||
seriesIndex: 0,
|
||||
show: false,
|
||||
pieces: [
|
||||
{
|
||||
max: 0.0,
|
||||
min: absoluteMin.value,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
max: absoluteMax.value,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_ABS_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 40,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_TRADE_COUNT,
|
||||
nameRotate: 90,
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_ABS_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 40,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_TRADE_COUNT,
|
||||
nameRotate: 90,
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-chart
|
||||
<e-charts
|
||||
v-if="trades.length > 0"
|
||||
:option="hourlyChartOptions"
|
||||
autoresize
|
||||
|
@ -7,10 +7,10 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import ECharts from 'vue-echarts';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Trade } from '@/types';
|
||||
import { timestampHour } from '@/shared/formatters';
|
||||
|
@ -45,121 +45,112 @@ use([
|
|||
const CHART_PROFIT = 'Profit %';
|
||||
const CHART_TRADE_COUNT = 'Trade Count';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HourlyChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => Trade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const props = defineProps({
|
||||
trades: { required: true, type: Array as () => Trade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const hourlyData = computed(() => {
|
||||
const res = new Array(24);
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
res[i] = { hour: i, hourDesc: `${i}h`, profit: 0.0, count: 0.0 };
|
||||
}
|
||||
const hourlyData = computed(() => {
|
||||
const res = new Array(24);
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
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) {
|
||||
const trade = props.trades[i];
|
||||
if (trade.close_timestamp) {
|
||||
const hour = timestampHour(trade.close_timestamp);
|
||||
for (let i = 0, len = props.trades.length; i < len; i += 1) {
|
||||
const trade = props.trades[i];
|
||||
if (trade.close_timestamp) {
|
||||
const hour = timestampHour(trade.close_timestamp);
|
||||
|
||||
res[hour].profit += trade.profit_ratio;
|
||||
res[hour].count += 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
});
|
||||
const hourlyChartOptions = computed((): EChartsOption => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Hourly Profit',
|
||||
show: props.showTitle,
|
||||
res[hour].profit += trade.profit_ratio;
|
||||
res[hour].count += 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
});
|
||||
const hourlyChartOptions = computed((): EChartsOption => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Hourly Profit',
|
||||
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'],
|
||||
source: hourlyData.value,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [CHART_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [CHART_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_TRADE_COUNT,
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
},
|
||||
],
|
||||
visualMap: [
|
||||
{
|
||||
dimension: 1,
|
||||
seriesIndex: 0,
|
||||
show: false,
|
||||
pieces: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
max: 0.0,
|
||||
min: -2,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_TRADE_COUNT,
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
min: 0.0,
|
||||
max: 2,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
visualMap: [
|
||||
{
|
||||
dimension: 1,
|
||||
seriesIndex: 0,
|
||||
show: false,
|
||||
pieces: [
|
||||
{
|
||||
max: 0.0,
|
||||
min: -2,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
max: 2,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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 };
|
||||
},
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
</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>
|
||||
<div v-if="columns">
|
||||
<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>
|
||||
<div class="col-mb-3">
|
||||
<hr />
|
||||
|
||||
<b-form-group label="Target" label-for="FieldSel">
|
||||
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="3">
|
||||
</b-form-select>
|
||||
<b-form-group label="Target Plot" label-for="FieldSel">
|
||||
<edit-value
|
||||
v-model="selSubPlot"
|
||||
:allow-edit="!isMainPlot"
|
||||
allow-add
|
||||
editable-name="plot configuration"
|
||||
align-vertical
|
||||
@new="addSubplot"
|
||||
@delete="deleteSubplot"
|
||||
@rename="renameSubplot"
|
||||
>
|
||||
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="5">
|
||||
</b-form-select>
|
||||
</edit-value>
|
||||
</b-form-group>
|
||||
</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 />
|
||||
<div>
|
||||
<b-form-group label="Used indicators" label-for="selectedIndicators">
|
||||
<b-form-group label="Indicators in this plot" label-for="selectedIndicators">
|
||||
<b-form-select
|
||||
id="selectedIndicators"
|
||||
v-model="selIndicatorName"
|
||||
:disabled="addNewIndicator"
|
||||
:options="usedColumns"
|
||||
:select-size="4"
|
||||
>
|
||||
</b-form-select>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div>
|
||||
<b-button
|
||||
variant="primary"
|
||||
title="Add indicator to plot"
|
||||
size="sm"
|
||||
:disabled="addNewIndicator"
|
||||
@click="addNewIndicator = !addNewIndicator"
|
||||
>
|
||||
Add new indicator
|
||||
</b-button>
|
||||
<div class="d-flex flex-row mt-1">
|
||||
<b-button
|
||||
variant="secondary"
|
||||
title="Remove indicator to plot"
|
||||
size="sm"
|
||||
:disabled="!selIndicatorName"
|
||||
class="ms-1"
|
||||
class="col"
|
||||
@click="removeIndicator"
|
||||
>
|
||||
Remove indicator
|
||||
</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>
|
||||
|
||||
<PlotIndicator
|
||||
v-if="selIndicatorName || addNewIndicator"
|
||||
<PlotIndicatorSelect
|
||||
v-if="addNewIndicator"
|
||||
:columns="columns"
|
||||
class="mt-1"
|
||||
label="Select indicator to add"
|
||||
@indicator-selected="addNewIndicatorSelected"
|
||||
/>
|
||||
|
||||
<plot-indicator
|
||||
v-if="selIndicatorName"
|
||||
v-model="selIndicator"
|
||||
class="mt-1"
|
||||
:columns="columns"
|
||||
:add-new="addNewIndicator"
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<b-button class="ms-1" variant="secondary" size="sm" @click="loadPlotConfig">Load</b-button>
|
||||
<div class="d-flex flex-row">
|
||||
<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
|
||||
:disabled="
|
||||
(botStore.activeBot.isWebserverMode && botStore.activeBot.botApiVersion < 2.23) ||
|
||||
!botStore.activeBot.isBotOnline
|
||||
!botStore.activeBot.isBotOnline ||
|
||||
addNewIndicator
|
||||
"
|
||||
class="ms-1"
|
||||
class="ms-1 col"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="loadPlotConfigFromStrategy"
|
||||
>
|
||||
From strategy
|
||||
</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
|
||||
id="showButton"
|
||||
class="ms-1"
|
||||
class="ms-1 col"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
:disabled="addNewIndicator"
|
||||
title="Show configuration for easy transfer to a strategy"
|
||||
@click="showConfig = !showConfig"
|
||||
>Show</b-button
|
||||
>{{ showConfig ? 'Hide' : 'Show' }}</b-button
|
||||
>
|
||||
|
||||
<b-button
|
||||
v-if="showConfig"
|
||||
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"
|
||||
class="ms-1 col"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
data-toggle="tooltip"
|
||||
:disabled="addNewIndicator"
|
||||
title="Save configuration"
|
||||
@click="savePlotConfig"
|
||||
>Save</b-button
|
||||
>
|
||||
</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">
|
||||
<b-form-textarea
|
||||
id="TextArea"
|
||||
|
@ -129,14 +156,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlotConfig, EMPTY_PLOTCONFIG, IndicatorConfig } from '@/types';
|
||||
import { getCustomPlotConfig } from '@/shared/storage';
|
||||
import EditValue from '@/components/general/EditValue.vue';
|
||||
import PlotConfigSelect from '@/components/charts/PlotConfigSelect.vue';
|
||||
import PlotIndicator from '@/components/charts/PlotIndicator.vue';
|
||||
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 { usePlotConfigStore } from '@/stores/plotConfig';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import randomColor from '@/shared/randomColor';
|
||||
|
||||
defineProps({
|
||||
columns: { required: true, type: Array as () => string[] },
|
||||
|
@ -146,10 +177,7 @@ defineProps({
|
|||
const plotStore = usePlotConfigStore();
|
||||
const botStore = useBotStore();
|
||||
|
||||
const plotConfig = ref<PlotConfig>(EMPTY_PLOTCONFIG);
|
||||
|
||||
const plotConfigNameLoc = ref('default');
|
||||
const newSubplotName = ref('');
|
||||
const selIndicatorName = ref('');
|
||||
const addNewIndicator = ref(false);
|
||||
const showConfig = ref(false);
|
||||
|
@ -163,43 +191,40 @@ const isMainPlot = computed(() => {
|
|||
|
||||
const currentPlotConfig = computed(() => {
|
||||
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[] => {
|
||||
// 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[] => {
|
||||
if (isMainPlot.value) {
|
||||
return Object.keys(plotConfig.value.main_plot);
|
||||
return Object.keys(plotStore.editablePlotConfig.main_plot);
|
||||
}
|
||||
if (selSubPlot.value in plotConfig.value.subplots) {
|
||||
return Object.keys(plotConfig.value.subplots[selSubPlot.value]);
|
||||
if (selSubPlot.value in plotStore.editablePlotConfig.subplots) {
|
||||
return Object.keys(plotStore.editablePlotConfig.subplots[selSubPlot.value]);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
function addIndicator(newIndicator: Record<string, IndicatorConfig>) {
|
||||
console.log(plotConfig.value);
|
||||
|
||||
// const { plotConfig.value } = this;
|
||||
const name = Object.keys(newIndicator)[0];
|
||||
const indicator = newIndicator[name];
|
||||
if (isMainPlot.value) {
|
||||
console.log(`Adding ${name} to MainPlot`);
|
||||
plotConfig.value.main_plot[name] = { ...indicator };
|
||||
// console.log(`Adding ${name} to MainPlot`);
|
||||
plotStore.editablePlotConfig.main_plot[name] = { ...indicator };
|
||||
} else {
|
||||
console.log(`Adding ${name} to ${selSubPlot.value}`);
|
||||
plotConfig.value.subplots[selSubPlot.value][name] = { ...indicator };
|
||||
// console.log(`Adding ${name} to ${selSubPlot.value}`);
|
||||
plotStore.editablePlotConfig.subplots[selSubPlot.value][name] = { ...indicator };
|
||||
}
|
||||
|
||||
plotConfig.value = { ...plotConfig.value };
|
||||
plotStore.editablePlotConfig = { ...plotStore.editablePlotConfig };
|
||||
// Reset random color
|
||||
addNewIndicator.value = false;
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
}
|
||||
|
||||
const selIndicator = computed({
|
||||
|
@ -215,7 +240,6 @@ const selIndicator = computed({
|
|||
return {};
|
||||
},
|
||||
set(newValue: Record<string, IndicatorConfig>) {
|
||||
// console.log('newValue', newValue);
|
||||
const name = Object.keys(newValue)[0];
|
||||
// this.currentPlotConfig[this.selIndicatorName] = { ...newValue[name] };
|
||||
// this.emitPlotConfig();
|
||||
|
@ -229,7 +253,7 @@ const selIndicator = computed({
|
|||
|
||||
const plotConfigJson = computed({
|
||||
get() {
|
||||
return JSON.stringify(plotConfig.value, null, 2);
|
||||
return JSON.stringify(plotStore.editablePlotConfig, null, 2);
|
||||
},
|
||||
set(newValue: string) {
|
||||
try {
|
||||
|
@ -243,55 +267,53 @@ const plotConfigJson = computed({
|
|||
});
|
||||
|
||||
function removeIndicator() {
|
||||
console.log(plotConfig.value);
|
||||
// const { plotConfig } = this;
|
||||
if (isMainPlot.value) {
|
||||
console.log(`Removing ${selIndicatorName.value} from MainPlot`);
|
||||
delete plotConfig.value.main_plot[selIndicatorName.value];
|
||||
delete plotStore.editablePlotConfig.main_plot[selIndicatorName.value];
|
||||
} else {
|
||||
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 };
|
||||
console.log(plotConfig.value);
|
||||
plotStore.editablePlotConfig = { ...plotStore.editablePlotConfig };
|
||||
selIndicatorName.value = '';
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
}
|
||||
function addSubplot() {
|
||||
plotConfig.value.subplots = {
|
||||
...plotConfig.value.subplots,
|
||||
[newSubplotName.value]: {},
|
||||
function addSubplot(newSubplotName: string) {
|
||||
plotStore.editablePlotConfig.subplots = {
|
||||
...plotStore.editablePlotConfig.subplots,
|
||||
[newSubplotName]: {},
|
||||
};
|
||||
selSubPlot.value = newSubplotName.value;
|
||||
newSubplotName.value = '';
|
||||
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
selSubPlot.value = newSubplotName;
|
||||
}
|
||||
|
||||
function delSubplot() {
|
||||
delete plotConfig.value.subplots[selSubPlot.value];
|
||||
plotConfig.value.subplots = { ...plotConfig.value.subplots };
|
||||
selSubPlot.value = '';
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
function deleteSubplot(subplotName: string) {
|
||||
delete plotStore.editablePlotConfig.subplots[subplotName];
|
||||
// plotStore.editablePlotConfig.subplots = { ...plotStore.editablePlotConfig.subplots };
|
||||
selSubPlot.value = subplots.value[subplots.value.length - 1];
|
||||
}
|
||||
|
||||
function renameSubplot(oldName: string, newName: string) {
|
||||
plotStore.editablePlotConfig.subplots[newName] = plotStore.editablePlotConfig.subplots[oldName];
|
||||
delete plotStore.editablePlotConfig.subplots[oldName];
|
||||
selSubPlot.value = newName;
|
||||
}
|
||||
|
||||
function loadPlotConfig() {
|
||||
plotConfig.value = getCustomPlotConfig(plotConfigNameLoc.value);
|
||||
console.log(plotConfig.value);
|
||||
console.log('loading config');
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
// Reset from store
|
||||
plotStore.editablePlotConfig = deepClone(plotStore.customPlotConfigs[plotStore.plotConfigName]);
|
||||
}
|
||||
|
||||
function loadConfigFromString() {
|
||||
// this.plotConfig = JSON.parse();
|
||||
if (tempPlotConfig.value !== undefined && tempPlotConfigValid.value) {
|
||||
plotConfig.value = tempPlotConfig.value;
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
plotStore.editablePlotConfig = tempPlotConfig.value;
|
||||
}
|
||||
}
|
||||
function resetConfig() {
|
||||
plotConfig.value = { ...EMPTY_PLOTCONFIG };
|
||||
}
|
||||
|
||||
// function clearConfig() {
|
||||
// // Use empty config
|
||||
// plotStore.editablePlotConfig = { ...EMPTY_PLOTCONFIG };
|
||||
// }
|
||||
|
||||
async function loadPlotConfigFromStrategy() {
|
||||
if (botStore.activeBot.isWebserverMode && !botStore.activeBot.strategy.strategy) {
|
||||
showAlert(`No strategy selected, can't load plot config.`);
|
||||
|
@ -300,8 +322,7 @@ async function loadPlotConfigFromStrategy() {
|
|||
try {
|
||||
await botStore.activeBot.getStrategyPlotConfig();
|
||||
if (botStore.activeBot.strategyPlotConfig) {
|
||||
plotConfig.value = botStore.activeBot.strategyPlotConfig;
|
||||
plotStore.setPlotConfig(plotConfig.value);
|
||||
plotStore.editablePlotConfig = botStore.activeBot.strategyPlotConfig;
|
||||
}
|
||||
} catch (data) {
|
||||
//
|
||||
|
@ -310,14 +331,45 @@ async function loadPlotConfigFromStrategy() {
|
|||
}
|
||||
|
||||
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(() => {
|
||||
// console.log('Config Mounted', props);
|
||||
plotConfig.value = plotStore.plotConfig;
|
||||
// Deep clone and assign to editable
|
||||
plotStore.editablePlotConfig = deepClone(plotStore.plotConfig);
|
||||
plotStore.isEditing = true;
|
||||
plotConfigNameLoc.value = plotStore.plotConfigName;
|
||||
});
|
||||
onUnmounted(() => {
|
||||
// TODO: Unmounted is not called when closing in Chart view
|
||||
plotStore.isEditing = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,133 +1,89 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="addNew">
|
||||
<b-form-group label="Add indicator" label-for="indicatorSelector">
|
||||
<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>
|
||||
<div class="d-flex flex-col flex-xl-row justify-content-between mt-1">
|
||||
<b-form-group class="col flex-grow-1" label="Type" label-for="plotTypeSelector">
|
||||
<b-form-select
|
||||
id="indicatorSelector"
|
||||
v-model="selAvailableIndicator"
|
||||
:options="filteredIndicators"
|
||||
:select-size="4"
|
||||
id="plotTypeSelector"
|
||||
v-model="graphType"
|
||||
size="sm"
|
||||
:options="availableGraphTypes"
|
||||
>
|
||||
</b-form-select>
|
||||
</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>
|
||||
|
||||
<b-form-group label="Type" label-for="plotTypeSelector">
|
||||
<b-form-select
|
||||
id="plotTypeSelector"
|
||||
v-model="graphType"
|
||||
size="sm"
|
||||
: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>
|
||||
<PlotIndicatorSelect
|
||||
v-if="graphType === ChartType.line"
|
||||
v-model="fillTo"
|
||||
:columns="columns"
|
||||
class="mt-1"
|
||||
label="Select indicator to add"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChartType, IndicatorConfig } from '@/types';
|
||||
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 { watchDebounced } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { required: true, type: Object as () => Record<string, IndicatorConfig> },
|
||||
columns: { required: true, type: Array as () => string[] },
|
||||
addNew: { required: true, type: Boolean },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const selColor = ref(randomColor());
|
||||
const graphType = ref<ChartType>(ChartType.line);
|
||||
const availableGraphTypes = ref(Object.keys(ChartType));
|
||||
const indicatorFilter = ref('');
|
||||
const selAvailableIndicator = ref('');
|
||||
const cancelled = ref(false);
|
||||
const fillTo = ref('');
|
||||
|
||||
const filteredIndicators = computed(() => {
|
||||
return props.columns.filter((col) =>
|
||||
col.toLowerCase().includes(indicatorFilter.value.toLowerCase()),
|
||||
);
|
||||
});
|
||||
|
||||
const newColor = () => {
|
||||
function newColor() {
|
||||
selColor.value = randomColor();
|
||||
};
|
||||
}
|
||||
|
||||
const combinedIndicator = computed(() => {
|
||||
const combinedIndicator = computed<IndicatorConfig>(() => {
|
||||
if (cancelled.value || !selAvailableIndicator.value) {
|
||||
return {};
|
||||
}
|
||||
const val: IndicatorConfig = {
|
||||
color: selColor.value,
|
||||
type: graphType.value,
|
||||
};
|
||||
if (fillTo.value && graphType.value === ChartType.line) {
|
||||
val.fill_to = fillTo.value;
|
||||
}
|
||||
return {
|
||||
[selAvailableIndicator.value]: {
|
||||
color: selColor.value,
|
||||
type: graphType.value,
|
||||
},
|
||||
[selAvailableIndicator.value]: val,
|
||||
};
|
||||
});
|
||||
const emitIndicator = () => {
|
||||
emit('update:modelValue', combinedIndicator.value);
|
||||
};
|
||||
|
||||
const clickCancel = () => {
|
||||
cancelled.value = true;
|
||||
emitIndicator();
|
||||
};
|
||||
function emitIndicator() {
|
||||
emit('update:modelValue', combinedIndicator.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
|
@ -135,29 +91,26 @@ watch(
|
|||
[selAvailableIndicator.value] = Object.keys(props.modelValue);
|
||||
cancelled.value = false;
|
||||
if (selAvailableIndicator.value && props.modelValue) {
|
||||
selColor.value = props.modelValue[selAvailableIndicator.value].color || randomColor();
|
||||
graphType.value = props.modelValue[selAvailableIndicator.value].type || ChartType.line;
|
||||
const xx = props.modelValue[selAvailableIndicator.value];
|
||||
selColor.value = xx.color || randomColor();
|
||||
graphType.value = xx.type || ChartType.line;
|
||||
fillTo.value = xx.fill_to || '';
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch(selColor, () => {
|
||||
if (!props.addNew) {
|
||||
watchDebounced(
|
||||
[selColor, graphType, fillTo],
|
||||
() => {
|
||||
emitIndicator();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
debounce: 200,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.colorbox {
|
||||
border-radius: 50%;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
vertical-align: center;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<style scoped></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>
|
||||
<div class="d-flex flex-column h-100 position-relative">
|
||||
<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>
|
||||
<b-form-group
|
||||
class="w-25 order-1"
|
||||
class="order-1"
|
||||
:class="showTitle ? 'ms-5 ps-5' : 'position-absolute'"
|
||||
label="Bins"
|
||||
style="width: 33%; min-width: 12rem"
|
||||
label-for="input-bins"
|
||||
label-cols="6"
|
||||
content-cols="6"
|
||||
|
@ -16,14 +17,15 @@
|
|||
id="input-bins"
|
||||
v-model="settingsStore.profitDistributionBins"
|
||||
size="sm"
|
||||
class="mt-1"
|
||||
:options="binOptions"
|
||||
></b-form-select>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -57,92 +59,82 @@ use([
|
|||
// Define Column labels here to avoid typos
|
||||
const CHART_PROFIT = 'Trade count';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ProfitDistributionChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
// 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);
|
||||
const props = defineProps({
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
// 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 chartOptionsLoc: EChartsOption = {
|
||||
title: {
|
||||
text: 'Profit distribution',
|
||||
show: props.showTitle,
|
||||
const chartOptions = computed((): EChartsOption => {
|
||||
const chartOptionsLoc: EChartsOption = {
|
||||
title: {
|
||||
text: 'Profit distribution',
|
||||
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: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
// },
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 35,
|
||||
position: 'left',
|
||||
},
|
||||
],
|
||||
// grid: {
|
||||
// bottom: 80,
|
||||
// },
|
||||
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
name: CHART_PROFIT,
|
||||
animation: true,
|
||||
encode: {
|
||||
x: 'x0',
|
||||
y: 'y0',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
name: CHART_PROFIT,
|
||||
animation: true,
|
||||
encode: {
|
||||
x: 'x0',
|
||||
y: 'y0',
|
||||
},
|
||||
|
||||
// symbol: 'none',
|
||||
},
|
||||
],
|
||||
};
|
||||
return chartOptionsLoc;
|
||||
});
|
||||
// console.log(chartOptions);
|
||||
return { settingsStore, chartOptions, binOptions };
|
||||
},
|
||||
// symbol: 'none',
|
||||
},
|
||||
],
|
||||
};
|
||||
return chartOptionsLoc;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-chart
|
||||
<e-charts
|
||||
v-if="trades.length > 0"
|
||||
:option="chartOptions"
|
||||
autoresize
|
||||
|
@ -7,7 +7,7 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -26,7 +26,7 @@ import {
|
|||
|
||||
import { ClosedTrade } from '@/types';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { timestampms } from '@/shared/formatters';
|
||||
import { dataZoomPartial } from '@/shared/charts/chartZoom';
|
||||
|
||||
|
@ -49,143 +49,132 @@ use([
|
|||
const CHART_PROFIT = 'Profit %';
|
||||
const CHART_COLOR = '#9be0a8';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TradesLogChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const chartData = computed(() => {
|
||||
const res: (number | string)[][] = [];
|
||||
const sortedTrades = props.trades
|
||||
.slice(0)
|
||||
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
|
||||
for (let i = 0, len = sortedTrades.length; i < len; i += 1) {
|
||||
const trade = sortedTrades[i];
|
||||
const entry = [
|
||||
i,
|
||||
(trade.profit_ratio * 100).toFixed(2),
|
||||
trade.pair,
|
||||
trade.botName,
|
||||
timestampms(trade.close_timestamp),
|
||||
trade.is_short === undefined || !trade.is_short ? 'Long' : 'Short',
|
||||
];
|
||||
res.push(entry);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
const props = defineProps({
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
const chartData = computed(() => {
|
||||
const res: (number | string)[][] = [];
|
||||
const sortedTrades = props.trades
|
||||
.slice(0)
|
||||
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
|
||||
for (let i = 0, len = sortedTrades.length; i < len; i += 1) {
|
||||
const trade = sortedTrades[i];
|
||||
const entry = [
|
||||
i,
|
||||
(trade.profit_ratio * 100).toFixed(2),
|
||||
trade.pair,
|
||||
trade.botName,
|
||||
timestampms(trade.close_timestamp),
|
||||
trade.is_short === undefined || !trade.is_short ? 'Long' : 'Short',
|
||||
];
|
||||
res.push(entry);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
const chartOptions = computed((): EChartsOption => {
|
||||
// const { chartData } = this;
|
||||
// Show a maximum of 50 trades by default - allowing to zoom out further.
|
||||
const datazoomStart =
|
||||
chartData.value.length > 0 ? (1 - 50 / chartData.value.length) * 100 : 100;
|
||||
return {
|
||||
title: {
|
||||
text: 'Trades log',
|
||||
show: props.showTitle,
|
||||
const chartOptions = computed((): EChartsOption => {
|
||||
// const { chartData } = this;
|
||||
// Show a maximum of 50 trades by default - allowing to zoom out further.
|
||||
const datazoomStart = chartData.value.length > 0 ? (1 - 50 / chartData.value.length) * 100 : 100;
|
||||
return {
|
||||
title: {
|
||||
text: 'Trades log',
|
||||
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'],
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
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',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
max: 0.0,
|
||||
color: '#f84960',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
color: '#2ed191',
|
||||
},
|
||||
],
|
||||
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: {
|
||||
color: CHART_COLOR,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return { settingsStore, chartData, chartOptions };
|
||||
},
|
||||
itemStyle: {
|
||||
color: CHART_COLOR,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
aria-label="Refresh"
|
||||
@click="botStore.activeBot.getBacktestHistory"
|
||||
>
|
||||
↻
|
||||
<i-mdi-refresh />
|
||||
</button>
|
||||
<p>
|
||||
Load Historic results from disk. You can click on multiple results to load all of them into
|
||||
|
@ -28,24 +28,15 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, onMounted } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { timestampms } from '@/shared/formatters';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const botStore = useBotStore();
|
||||
|
||||
onMounted(() => {
|
||||
botStore.activeBot.getBacktestHistory();
|
||||
});
|
||||
|
||||
return {
|
||||
timestampms,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
onMounted(() => {
|
||||
botStore.activeBot.getBacktestHistory();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
showRightBar ? 'col-md-8' : 'col-md-10'
|
||||
} candle-chart-container px-0 h-100 align-self-stretch`"
|
||||
:slider-position="sliderPosition"
|
||||
:freqai-model="freqaiModel"
|
||||
>
|
||||
</CandleChartContainer>
|
||||
<TradeListNav
|
||||
|
@ -65,6 +66,7 @@ import { ChartSliderPosition, Trade } from '@/types';
|
|||
defineProps({
|
||||
timeframe: { required: true, type: String },
|
||||
strategy: { required: true, type: String },
|
||||
freqaiModel: { required: false, default: undefined, type: String },
|
||||
timerange: { required: true, type: String },
|
||||
pairlist: { required: true, type: Array as () => string[] },
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { StrategyBacktestResult } from '@/types';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BacktestResultSelect',
|
||||
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,
|
||||
};
|
||||
defineProps({
|
||||
backtestHistory: {
|
||||
required: true,
|
||||
type: Object as () => Record<string, StrategyBacktestResult>,
|
||||
},
|
||||
selectedBacktestResultKey: { required: false, default: '', type: String },
|
||||
});
|
||||
const emit = defineEmits(['selectionChange']);
|
||||
const setBacktestResult = (key) => {
|
||||
emit('selectionChange', key);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -44,6 +44,15 @@
|
|||
>
|
||||
</b-table>
|
||||
</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">
|
||||
<TradeList
|
||||
|
@ -60,6 +69,7 @@
|
|||
<script setup lang="ts">
|
||||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
import { StrategyBacktestResult, Trade } from '@/types';
|
||||
import BacktestResultPeriodBreakdown from './BacktestResultPeriodBreakdown.vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
|
|
|
@ -4,20 +4,29 @@
|
|||
<label class="me-auto h3">Balance</label>
|
||||
<div class="float-end d-flex flex-row">
|
||||
<b-button
|
||||
v-if="canUseBotBalance"
|
||||
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"
|
||||
>
|
||||
<HideIcon v-if="hideSmallBalances" :size="16" />
|
||||
<ShowIcon v-else :size="16" />
|
||||
<i-mdi-eye-off v-if="hideSmallBalances" />
|
||||
<i-mdi-eye v-else />
|
||||
</b-button>
|
||||
|
||||
<b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance"
|
||||
>↻</b-button
|
||||
>
|
||||
<b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance">
|
||||
<i-mdi-refresh />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<BalanceChart v-if="balanceCurrencies" :currencies="balanceCurrencies" />
|
||||
<BalanceChart v-if="balanceCurrencies" :currencies="chartValues" />
|
||||
<div>
|
||||
<p v-if="botStore.activeBot.balance.note">
|
||||
<strong>{{ botStore.activeBot.balance.note }}</strong>
|
||||
|
@ -36,7 +45,11 @@
|
|||
</td>
|
||||
<!-- this is a computed prop that adds up all the expenses in the visible rows -->
|
||||
<td>
|
||||
<strong>{{ formatCurrency(botStore.activeBot.balance.total) }}</strong>
|
||||
<strong>{{
|
||||
showBotOnly && canUseBotBalance
|
||||
? formatCurrency(botStore.activeBot.balance.total_bot)
|
||||
: formatCurrency(botStore.activeBot.balance.total)
|
||||
}}</strong>
|
||||
</td>
|
||||
</template>
|
||||
</b-table>
|
||||
|
@ -45,38 +58,64 @@
|
|||
</template>
|
||||
|
||||
<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 { formatPercent, formatPrice } from '@/shared/formatters';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { BalanceValues } from '@/types';
|
||||
import { TableField } from 'bootstrap-vue-next';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const botStore = useBotStore();
|
||||
const hideSmallBalances = ref(true);
|
||||
const showBotOnly = ref(true);
|
||||
|
||||
const smallBalance = computed((): number => {
|
||||
return Number((0.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8));
|
||||
const smallBalance = computed<number>(() => {
|
||||
return Number((1.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8));
|
||||
});
|
||||
|
||||
const canUseBotBalance = computed(() => {
|
||||
return botStore.activeBot.botApiVersion >= 2.26;
|
||||
});
|
||||
|
||||
const balanceCurrencies = computed(() => {
|
||||
if (!hideSmallBalances.value) {
|
||||
return botStore.activeBot.balance.currencies;
|
||||
}
|
||||
|
||||
return botStore.activeBot.balance.currencies?.filter((v) => v.est_stake >= smallBalance.value);
|
||||
return botStore.activeBot.balance.currencies?.filter(
|
||||
(v) =>
|
||||
(!hideSmallBalances.value || v.est_stake >= smallBalance.value) &&
|
||||
(!canUseBotBalance.value || !showBotOnly.value || (v.is_bot_managed ?? true) === true),
|
||||
);
|
||||
});
|
||||
|
||||
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 [
|
||||
{ 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}`,
|
||||
formatter: formatCurrency,
|
||||
},
|
||||
|
|
|
@ -52,86 +52,73 @@
|
|||
</b-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { ProfitInterface, ComparisonTableItems } from '@/types';
|
||||
import { TableField, TableItem } from 'bootstrap-vue-next';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BotComparisonList',
|
||||
components: { ProfitPill },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const botStore = useBotStore();
|
||||
|
||||
const tableFields: TableField[] = [
|
||||
{ key: 'botName', label: 'Bot' },
|
||||
{ key: 'trades', label: 'Trades' },
|
||||
{ key: 'profitOpen', label: 'Open Profit' },
|
||||
{ key: 'profitClosed', label: 'Closed Profit' },
|
||||
{ key: 'balance', label: 'Balance' },
|
||||
{ key: 'winVsLoss', label: 'W/L' },
|
||||
];
|
||||
const tableFields: TableField[] = [
|
||||
{ key: 'botName', label: 'Bot' },
|
||||
{ key: 'trades', label: 'Trades' },
|
||||
{ key: 'profitOpen', label: 'Open Profit' },
|
||||
{ key: 'profitClosed', label: 'Closed Profit' },
|
||||
{ key: 'balance', label: 'Balance' },
|
||||
{ key: 'winVsLoss', label: 'W/L' },
|
||||
];
|
||||
|
||||
const tableItems = computed<TableItem[]>(() => {
|
||||
const val: ComparisonTableItems[] = [];
|
||||
const summary: ComparisonTableItems = {
|
||||
botId: undefined,
|
||||
botName: 'Summary',
|
||||
profitClosed: 0,
|
||||
profitClosedRatio: undefined,
|
||||
profitOpen: 0,
|
||||
profitOpenRatio: undefined,
|
||||
stakeCurrency: 'USDT',
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
const tableItems = computed<TableItem[]>(() => {
|
||||
const val: ComparisonTableItems[] = [];
|
||||
const summary: ComparisonTableItems = {
|
||||
botId: undefined,
|
||||
botName: 'Summary',
|
||||
profitClosed: 0,
|
||||
profitClosedRatio: undefined,
|
||||
profitOpen: 0,
|
||||
profitOpenRatio: undefined,
|
||||
stakeCurrency: 'USDT',
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
|
||||
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 profitOpenRatio =
|
||||
botStore.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) /
|
||||
allStakes;
|
||||
const profitOpen = botStore.allOpenTrades[k].reduce((a, b) => a + (b.profit_abs ?? 0), 0);
|
||||
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 profitOpenRatio =
|
||||
botStore.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) /
|
||||
allStakes;
|
||||
const profitOpen = botStore.allOpenTrades[k].reduce((a, b) => a + (b.profit_abs ?? 0), 0);
|
||||
|
||||
// TODO: handle one inactive bot ...
|
||||
val.push({
|
||||
botId: k,
|
||||
botName: botStore.availableBots[k].botName,
|
||||
trades: `${botStore.allOpenTradeCount[k]} / ${
|
||||
botStore.allBotState[k]?.max_open_trades || 'N/A'
|
||||
}`,
|
||||
profitClosed: v.profit_closed_coin,
|
||||
profitClosedRatio: v.profit_closed_ratio || 0,
|
||||
stakeCurrency: botStore.allBotState[k]?.stake_currency || '',
|
||||
profitOpenRatio,
|
||||
profitOpen,
|
||||
wins: v.winning_trades,
|
||||
losses: v.losing_trades,
|
||||
balance: botStore.allBalance[k]?.total,
|
||||
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[];
|
||||
// TODO: handle one inactive bot ...
|
||||
val.push({
|
||||
botId: k,
|
||||
botName: botStore.availableBots[k].botName,
|
||||
trades: `${botStore.allOpenTradeCount[k]} / ${
|
||||
botStore.allBotState[k]?.max_open_trades || 'N/A'
|
||||
}`,
|
||||
profitClosed: v.profit_closed_coin,
|
||||
profitClosedRatio: v.profit_closed_ratio || 0,
|
||||
stakeCurrency: botStore.allBotState[k]?.stake_currency || '',
|
||||
profitOpenRatio,
|
||||
profitOpen,
|
||||
wins: v.winning_trades,
|
||||
losses: v.losing_trades,
|
||||
balance: botStore.allBalance[k]?.total_bot ?? botStore.allBalance[k]?.total,
|
||||
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
|
||||
});
|
||||
|
||||
return {
|
||||
formatPrice,
|
||||
tableFields,
|
||||
tableItems,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
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[];
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ forceexit
|
|||
title="Start Trading"
|
||||
@click="botStore.activeBot.startBot()"
|
||||
>
|
||||
<PlayIcon />
|
||||
<i-mdi-play height="24" width="24" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ms-1"
|
||||
|
@ -15,7 +15,7 @@ forceexit
|
|||
title="Stop Trading - Also stops handling open trades."
|
||||
@click="handleStopBot()"
|
||||
>
|
||||
<StopIcon />
|
||||
<i-mdi-stop height="24" width="24" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ms-1"
|
||||
|
@ -23,7 +23,7 @@ forceexit
|
|||
title="StopBuy - Stops buying, but still handles open trades"
|
||||
@click="handleStopBuy()"
|
||||
>
|
||||
<PauseIcon />
|
||||
<i-mdi-pause height="24" width="24" />
|
||||
</button>
|
||||
<button
|
||||
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."
|
||||
@click="handleReloadConfig()"
|
||||
>
|
||||
<ReloadIcon />
|
||||
<i-mdi-reload height="24" width="24" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ms-1"
|
||||
|
@ -39,20 +39,16 @@ forceexit
|
|||
title="Force exit all"
|
||||
@click="handleForceExit()"
|
||||
>
|
||||
<ForceExitIcon />
|
||||
<i-mdi-close-box-multiple height="24" width="24" />
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
botStore.activeBot.botState &&
|
||||
(botStore.activeBot.botState.force_entry_enable ||
|
||||
botStore.activeBot.botState.forcebuy_enabled)
|
||||
"
|
||||
v-if="botStore.activeBot.botState && botStore.activeBot.botState.force_entry_enable"
|
||||
class="btn btn-secondary btn-sm ms-1"
|
||||
:disabled="!botStore.activeBot.isTrading || !isRunning"
|
||||
title="Force enter - Immediately enter a trade at an optional price. Exits are then handled according to strategy rules."
|
||||
@click="forceEnter = true"
|
||||
>
|
||||
<ForceEntryIcon />
|
||||
<i-mdi-plus-box-multiple-outline style="font-size: 20px" />
|
||||
</button>
|
||||
<button
|
||||
v-if="botStore.activeBot.isWebserverMode && false"
|
||||
|
@ -61,105 +57,75 @@ forceexit
|
|||
title="Start Trading mode"
|
||||
@click="botStore.activeBot.startTrade()"
|
||||
>
|
||||
<PlayIcon />
|
||||
<i-mdi-play class="fs-4" />
|
||||
</button>
|
||||
<ForceEntryForm v-model="forceEnter" :pair="botStore.activeBot.selectedPair" />
|
||||
<MessageBox ref="msgBox" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script 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';
|
||||
<script setup lang="ts">
|
||||
import MessageBox, { MsgBoxObject } from '@/components/general/MessageBox.vue';
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { ForceSellPayload } from '@/types';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
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>();
|
||||
import ForceEntryForm from './ForceEntryForm.vue';
|
||||
|
||||
const isRunning = computed((): boolean => {
|
||||
return botStore.activeBot.botState?.state === 'running';
|
||||
});
|
||||
const botStore = useBotStore();
|
||||
const forceEnter = ref<boolean>(false);
|
||||
const msgBox = ref<typeof MessageBox>();
|
||||
|
||||
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);
|
||||
};
|
||||
return {
|
||||
handleStopBot,
|
||||
handleStopBuy,
|
||||
handleReloadConfig,
|
||||
handleForceExit,
|
||||
forceEnter,
|
||||
botStore,
|
||||
isRunning,
|
||||
msgBox,
|
||||
};
|
||||
},
|
||||
const isRunning = computed((): boolean => {
|
||||
return botStore.activeBot.botState?.state === 'running';
|
||||
});
|
||||
|
||||
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>
|
||||
|
|
|
@ -11,32 +11,23 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { TableField } from 'bootstrap-vue-next';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BotPerformance',
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const tableFields = computed<TableField[]>(() => {
|
||||
return [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'profit', label: 'Profit %' },
|
||||
{
|
||||
key: 'profit_abs',
|
||||
label: `Profit ${botStore.activeBot.botState?.stake_currency}`,
|
||||
formatter: (v: unknown) => formatPrice(v as number, 5),
|
||||
},
|
||||
{ key: 'count', label: 'Count' },
|
||||
];
|
||||
});
|
||||
return {
|
||||
tableFields,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
const botStore = useBotStore();
|
||||
const tableFields = computed<TableField[]>(() => {
|
||||
return [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'profit', label: 'Profit %' },
|
||||
{
|
||||
key: 'profit_abs',
|
||||
label: `Profit ${botStore.activeBot.botState?.stake_currency}`,
|
||||
formatter: (v: unknown) => formatPrice(v as number, 5),
|
||||
},
|
||||
{ key: 'count', label: 'Count' },
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -27,13 +27,7 @@
|
|||
<p>
|
||||
Currently <strong>{{ botStore.activeBot.botState.state }}</strong
|
||||
>,
|
||||
<strong
|
||||
>force entry:
|
||||
{{
|
||||
botStore.activeBot.botState.force_entry_enable ||
|
||||
botStore.activeBot.botState.forcebuy_enabled
|
||||
}}</strong
|
||||
>
|
||||
<strong>force entry: {{ botStore.activeBot.botState.force_entry_enable }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ botStore.activeBot.botState.dry_run ? 'Dry-Run' : 'Live' }}</strong>
|
||||
|
@ -85,23 +79,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BotStatus',
|
||||
components: { DateTimeTZ },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
return {
|
||||
formatPercent,
|
||||
formatPriceCurrency,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
const botStore = useBotStore();
|
||||
</script>
|
||||
|
|
|
@ -33,70 +33,36 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
<script setup lang="ts">
|
||||
import { Trade } from '@/types';
|
||||
import CustomTradeListEntry from '@/components/ftbot/CustomTradeListEntry.vue';
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CustomTradeList',
|
||||
components: {
|
||||
CustomTradeListEntry,
|
||||
},
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => Trade[] },
|
||||
title: { default: 'Trades', type: String },
|
||||
stakeCurrency: { required: false, default: '', type: String },
|
||||
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 props = defineProps({
|
||||
trades: { required: true, type: Array as () => Trade[] },
|
||||
title: { default: 'Trades', type: String },
|
||||
stakeCurrency: { required: false, default: '', type: String },
|
||||
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 },
|
||||
});
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -15,39 +15,23 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||
<script setup lang="ts">
|
||||
import { Trade } from '@/types';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
import TradeProfit from './TradeProfit.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DateTimeTZ,
|
||||
TradeProfit,
|
||||
defineProps({
|
||||
trade: {
|
||||
type: Object as () => Trade,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
trade: {
|
||||
type: Object as () => Trade,
|
||||
required: true,
|
||||
},
|
||||
stakeCurrencyDecimals: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showDetails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
stakeCurrencyDecimals: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
formatPrice,
|
||||
formatPercent,
|
||||
};
|
||||
showDetails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div>
|
||||
<div class="mb-2">
|
||||
<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>
|
||||
<DailyChart
|
||||
|
@ -17,51 +19,38 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, onMounted } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import DailyChart from '@/components/charts/DailyChart.vue';
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { TableField } from 'bootstrap-vue-next';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DailyStats',
|
||||
components: {
|
||||
DailyChart,
|
||||
},
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const dailyFields = computed<TableField[]>(() => {
|
||||
const res: TableField[] = [
|
||||
{ key: 'date', label: 'Day' },
|
||||
{
|
||||
key: 'abs_profit',
|
||||
label: 'Profit',
|
||||
// formatter: (value: unknown) => formatPrice(value as number),
|
||||
},
|
||||
{
|
||||
key: 'fiat_value',
|
||||
label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`,
|
||||
// formatter: (value: unknown) => formatPrice(value as number, 2),
|
||||
},
|
||||
{ key: 'trade_count', label: 'Trades' },
|
||||
];
|
||||
if (botStore.activeBot.botApiVersion >= 2.16)
|
||||
res.push({
|
||||
key: 'rel_profit',
|
||||
label: 'Profit%',
|
||||
formatter: (value: unknown) => formatPercent(value as number, 2),
|
||||
});
|
||||
return res;
|
||||
const botStore = useBotStore();
|
||||
const dailyFields = computed<TableField[]>(() => {
|
||||
const res: TableField[] = [
|
||||
{ key: 'date', label: 'Day' },
|
||||
{
|
||||
key: 'abs_profit',
|
||||
label: 'Profit',
|
||||
// formatter: (value: unknown) => formatPrice(value as number),
|
||||
},
|
||||
{
|
||||
key: 'fiat_value',
|
||||
label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`,
|
||||
// formatter: (value: unknown) => formatPrice(value as number, 2),
|
||||
},
|
||||
{ key: 'trade_count', label: 'Trades' },
|
||||
];
|
||||
if (botStore.activeBot.botApiVersion >= 2.16)
|
||||
res.push({
|
||||
key: 'rel_profit',
|
||||
label: 'Profit%',
|
||||
formatter: (value: unknown) => formatPercent(value as number, 2),
|
||||
});
|
||||
onMounted(() => {
|
||||
botStore.activeBot.getDaily();
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
dailyFields,
|
||||
};
|
||||
},
|
||||
return res;
|
||||
});
|
||||
onMounted(() => {
|
||||
botStore.activeBot.getDaily();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
class="me-1"
|
||||
:class="botStore.activeBot.botApiVersion >= 1.12 ? 'col-6' : ''"
|
||||
size="sm"
|
||||
>+
|
||||
><i-mdi-plus-box-outline />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="botStore.activeBot.botApiVersion >= 1.12"
|
||||
|
@ -43,7 +43,7 @@
|
|||
:disabled="blacklistSelect.length === 0"
|
||||
@click="deletePairs"
|
||||
>
|
||||
<DeleteIcon :size="16" title="Delete Bot" />
|
||||
<i-mdi-delete />
|
||||
</b-button>
|
||||
</div>
|
||||
<b-popover
|
||||
|
@ -81,7 +81,7 @@
|
|||
class="pair black"
|
||||
:active="blacklistSelect.indexOf(key) > -1"
|
||||
@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>
|
||||
</div>
|
||||
|
@ -91,89 +91,73 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import { defineComponent, ref, onMounted } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FTBotAPIPairList',
|
||||
components: { DeleteIcon },
|
||||
setup() {
|
||||
const newblacklistpair = ref('');
|
||||
const blackListShow = ref(false);
|
||||
const blacklistSelect = ref<number[]>([]);
|
||||
const botStore = useBotStore();
|
||||
const newblacklistpair = ref('');
|
||||
const blackListShow = ref(false);
|
||||
const blacklistSelect = ref<number[]>([]);
|
||||
const botStore = useBotStore();
|
||||
|
||||
const initBlacklist = () => {
|
||||
if (botStore.activeBot.whitelist.length === 0) {
|
||||
botStore.activeBot.getWhitelist();
|
||||
}
|
||||
if (botStore.activeBot.blacklist.length === 0) {
|
||||
botStore.activeBot.getBlacklist();
|
||||
}
|
||||
};
|
||||
const initBlacklist = () => {
|
||||
if (botStore.activeBot.whitelist.length === 0) {
|
||||
botStore.activeBot.getWhitelist();
|
||||
}
|
||||
if (botStore.activeBot.blacklist.length === 0) {
|
||||
botStore.activeBot.getBlacklist();
|
||||
}
|
||||
};
|
||||
|
||||
const addBlacklistPair = () => {
|
||||
if (newblacklistpair.value) {
|
||||
blackListShow.value = false;
|
||||
const addBlacklistPair = () => {
|
||||
if (newblacklistpair.value) {
|
||||
blackListShow.value = false;
|
||||
|
||||
botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] });
|
||||
newblacklistpair.value = '';
|
||||
}
|
||||
};
|
||||
botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] });
|
||||
newblacklistpair.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const blacklistSelectClick = (key) => {
|
||||
console.log(key);
|
||||
const index = blacklistSelect.value.indexOf(key);
|
||||
if (index > -1) {
|
||||
blacklistSelect.value.splice(index, 1);
|
||||
} else {
|
||||
blacklistSelect.value.push(key);
|
||||
}
|
||||
};
|
||||
const blacklistSelectClick = (key) => {
|
||||
console.log(key);
|
||||
const index = blacklistSelect.value.indexOf(key);
|
||||
if (index > -1) {
|
||||
blacklistSelect.value.splice(index, 1);
|
||||
} else {
|
||||
blacklistSelect.value.push(key);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePairs = () => {
|
||||
if (blacklistSelect.value.length === 0) {
|
||||
console.log('nothing to delete');
|
||||
return;
|
||||
}
|
||||
// const pairlist = blacklistSelect.value;
|
||||
const pairlist = botStore.activeBot.blacklist.filter(
|
||||
(value, index) => blacklistSelect.value.indexOf(index) > -1,
|
||||
);
|
||||
console.log('Deleting pairs: ', pairlist);
|
||||
botStore.activeBot.deleteBlacklist(pairlist);
|
||||
blacklistSelect.value = [];
|
||||
};
|
||||
onMounted(() => {
|
||||
initBlacklist();
|
||||
});
|
||||
return {
|
||||
addBlacklistPair,
|
||||
deletePairs,
|
||||
initBlacklist,
|
||||
blacklistSelectClick,
|
||||
botStore,
|
||||
newblacklistpair,
|
||||
blackListShow,
|
||||
blacklistSelect,
|
||||
};
|
||||
},
|
||||
const deletePairs = () => {
|
||||
if (blacklistSelect.value.length === 0) {
|
||||
console.log('nothing to delete');
|
||||
return;
|
||||
}
|
||||
// const pairlist = blacklistSelect.value;
|
||||
const pairlist = botStore.activeBot.blacklist.filter(
|
||||
(value, index) => blacklistSelect.value.indexOf(index) > -1,
|
||||
);
|
||||
console.log('Deleting pairs: ', pairlist);
|
||||
botStore.activeBot.deleteBlacklist(pairlist);
|
||||
blacklistSelect.value = [];
|
||||
};
|
||||
onMounted(() => {
|
||||
initBlacklist();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.check {
|
||||
// Hidden checkbox on blacklist selection
|
||||
background: #41b883;
|
||||
// background: white;
|
||||
color: #41b883;
|
||||
opacity: 0;
|
||||
border-radius: 50%;
|
||||
// border-radius: 50%;
|
||||
z-index: 5;
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
top: -0.2em;
|
||||
left: -0.2em;
|
||||
top: -0.3em;
|
||||
left: -0.3em;
|
||||
position: absolute;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
|
|
@ -56,7 +56,6 @@
|
|||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
v-if="botStore.activeBot.botApiVersion > 1.12"
|
||||
:label="`*Stake-amount in ${botStore.activeBot.stakeCurrency} [optional]`"
|
||||
label-for="stake-input"
|
||||
invalid-feedback="Stake-amount must be empty or a positive number"
|
||||
|
@ -86,7 +85,6 @@
|
|||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
v-if="botStore.activeBot.botApiVersion > 1.1"
|
||||
label="OrderType"
|
||||
label-for="ordertype-input"
|
||||
invalid-feedback="OrderType"
|
||||
|
@ -202,14 +200,12 @@ const resetForm = () => {
|
|||
selectedPair.value = props.pair;
|
||||
price.value = undefined;
|
||||
stakeAmount.value = undefined;
|
||||
if (botStore.activeBot.botApiVersion > 1.1) {
|
||||
ordertype.value =
|
||||
botStore.activeBot.botState?.order_types?.forcebuy ||
|
||||
botStore.activeBot.botState?.order_types?.force_entry ||
|
||||
botStore.activeBot.botState?.order_types?.buy ||
|
||||
botStore.activeBot.botState?.order_types?.entry ||
|
||||
'limit';
|
||||
}
|
||||
ordertype.value =
|
||||
botStore.activeBot.botState?.order_types?.forcebuy ||
|
||||
botStore.activeBot.botState?.order_types?.force_entry ||
|
||||
botStore.activeBot.botState?.order_types?.buy ||
|
||||
botStore.activeBot.botState?.order_types?.entry ||
|
||||
'limit';
|
||||
};
|
||||
|
||||
const handleEntry = () => {
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
<span>Currently owning {{ trade.amount }} {{ trade.base_currency }}</span>
|
||||
</p>
|
||||
<b-form-group
|
||||
v-if="botStore.activeBot.botApiVersion > 1.12"
|
||||
:label="`*Amount in ${trade.base_currency} [optional]`"
|
||||
label-for="stake-input"
|
||||
invalid-feedback="Amount must be empty or a positive number"
|
||||
|
@ -40,7 +39,6 @@
|
|||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-if="botStore.activeBot.botApiVersion > 1.1"
|
||||
label="*OrderType"
|
||||
label-for="ordertype-input"
|
||||
invalid-feedback="OrderType"
|
||||
|
@ -114,12 +112,10 @@ const handleSubmit = () => {
|
|||
};
|
||||
const resetForm = () => {
|
||||
amount.value = props.trade.amount;
|
||||
if (botStore.activeBot.botApiVersion > 1.1) {
|
||||
ordertype.value =
|
||||
botStore.activeBot.botState?.order_types?.force_exit ||
|
||||
botStore.activeBot.botState?.order_types?.exit ||
|
||||
'limit';
|
||||
}
|
||||
ordertype.value =
|
||||
botStore.activeBot.botState?.order_types?.force_exit ||
|
||||
botStore.activeBot.botState?.order_types?.exit ||
|
||||
'limit';
|
||||
};
|
||||
|
||||
const handleEntry = () => {
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
>
|
||||
</b-form-select>
|
||||
<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>
|
||||
|
|
|
@ -1,37 +1,29 @@
|
|||
<template>
|
||||
<div class="d-flex h-100 p-0 align-items-start">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { defineComponent, onMounted, computed } from 'vue';
|
||||
import { onMounted, computed } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LogViewer',
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const botStore = useBotStore();
|
||||
|
||||
onMounted(async () => {
|
||||
botStore.activeBot.getLogs();
|
||||
});
|
||||
onMounted(async () => {
|
||||
botStore.activeBot.getLogs();
|
||||
});
|
||||
|
||||
const formattedLogs = computed(() => {
|
||||
let result = '';
|
||||
for (let i = 0, len = botStore.activeBot.lastLogs.length; i < len; i += 1) {
|
||||
const log = botStore.activeBot.lastLogs[i];
|
||||
result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
formattedLogs,
|
||||
};
|
||||
},
|
||||
const formattedLogs = computed(() => {
|
||||
let result = '';
|
||||
for (let i = 0, len = botStore.activeBot.lastLogs.length; i < len; i += 1) {
|
||||
const log = botStore.activeBot.lastLogs[i];
|
||||
result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div>
|
||||
<div class="mb-2">
|
||||
<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>
|
||||
<b-table class="table-sm" :items="botStore.activeBot.activeLocks" :fields="tableFields">
|
||||
|
@ -13,7 +15,7 @@
|
|||
title="Delete trade"
|
||||
@click="removePairLock(row.item)"
|
||||
>
|
||||
<DeleteIcon :size="16" />
|
||||
<i-mdi-delete />
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
|
@ -21,45 +23,30 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { timestampms } from '@/shared/formatters';
|
||||
import { Lock } from '@/types';
|
||||
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import { showAlert } from '@/stores/alerts';
|
||||
import { defineComponent } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { TableField } from 'bootstrap-vue-next';
|
||||
const botStore = useBotStore();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PairLockList',
|
||||
components: { DeleteIcon },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const tableFields: TableField[] = [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'lock_end_timestamp', label: 'Until', formatter: (value) => timestampms(value as number) },
|
||||
{ key: 'reason', label: 'Reason' },
|
||||
{ key: 'actions' },
|
||||
];
|
||||
|
||||
const tableFields = [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'lock_end_timestamp', label: 'Until', formatter: 'timestampms' },
|
||||
{ key: 'reason', label: 'Reason' },
|
||||
{ key: 'actions' },
|
||||
];
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
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.');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
>
|
||||
<div>
|
||||
{{ comb.pair }}
|
||||
<span v-if="comb.locks" :title="comb.lockReason"> 🔒 </span>
|
||||
<span v-if="comb.locks" :title="comb.lockReason"> <i-mdi-lock /> </span>
|
||||
</div>
|
||||
|
||||
<TradeProfit v-if="comb.trade && !backtestMode" :trade="comb.trade" />
|
||||
|
|
|
@ -12,34 +12,22 @@
|
|||
title="Auto Refresh All bots"
|
||||
@click="botStore.allRefreshFull"
|
||||
>
|
||||
<RefreshIcon :size="16" />
|
||||
<i-mdi-refresh />
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import RefreshIcon from 'vue-material-design-icons/Refresh.vue';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ReloadControl',
|
||||
components: { RefreshIcon },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const autoRefreshLoc = computed({
|
||||
get() {
|
||||
return botStore.globalAutoRefresh;
|
||||
},
|
||||
set(newValue: boolean) {
|
||||
botStore.setGlobalAutoRefresh(newValue);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
autoRefreshLoc,
|
||||
};
|
||||
const botStore = useBotStore();
|
||||
const autoRefreshLoc = computed({
|
||||
get() {
|
||||
return botStore.globalAutoRefresh;
|
||||
},
|
||||
set(newValue: boolean) {
|
||||
botStore.setGlobalAutoRefresh(newValue);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
>
|
||||
</b-form-select>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -7,62 +7,50 @@
|
|||
></b-form-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TimefameSelect',
|
||||
props: {
|
||||
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 props = defineProps({
|
||||
value: { default: '', type: String },
|
||||
belowTimeframe: { required: false, default: '', type: String },
|
||||
});
|
||||
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>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
title="Forceexit"
|
||||
@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
|
||||
v-if="botApiVersion > 1.1"
|
||||
|
@ -16,7 +16,7 @@
|
|||
title="Forceexit 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
|
||||
v-if="botApiVersion > 1.1"
|
||||
|
@ -25,7 +25,7 @@
|
|||
title="Forceexit 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
|
||||
v-if="botApiVersion > 2.16"
|
||||
|
@ -34,7 +34,7 @@
|
|||
title="Forceexit partial"
|
||||
@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
|
||||
v-if="botApiVersion >= 2.24 && trade.open_order_id"
|
||||
|
@ -43,16 +43,24 @@
|
|||
title="Cancel open orders"
|
||||
@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
|
||||
class="btn-xs text-start mt-1"
|
||||
size="sm"
|
||||
title="Delete trade"
|
||||
@click="$emit('deleteTrade', trade)"
|
||||
>
|
||||
<DeleteIcon :size="16" title="Delete trade" class="me-1" />
|
||||
<i-mdi-delete class="me-1" />
|
||||
Delete
|
||||
</b-button>
|
||||
</div>
|
||||
|
@ -60,10 +68,6 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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({
|
||||
botApiVersion: {
|
||||
|
@ -75,7 +79,7 @@ defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'deleteTrade']);
|
||||
defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'reloadTrade', 'deleteTrade']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
<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 { ref } from 'vue';
|
||||
import TradeActions from './TradeActions.vue';
|
||||
|
||||
defineProps({
|
||||
trade: { type: Object as () => Trade, required: true },
|
||||
id: { 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 forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => {
|
||||
function forceExitHandler(item: Trade, ordertype: string | undefined = undefined) {
|
||||
popoverOpen.value = false;
|
||||
emit('forceExit', item, ordertype);
|
||||
};
|
||||
const forceExitPartialHandler = (item: Trade) => {
|
||||
}
|
||||
function forceExitPartialHandler(item: Trade) {
|
||||
popoverOpen.value = false;
|
||||
emit('forceExitPartial', item);
|
||||
};
|
||||
const cancelOpenOrderHandler = (item: Trade) => {
|
||||
}
|
||||
function cancelOpenOrderHandler(item: Trade) {
|
||||
popoverOpen.value = false;
|
||||
emit('cancelOpenOrder', item);
|
||||
};
|
||||
}
|
||||
function handleReloadTrade(item: Trade) {
|
||||
popoverOpen.value = false;
|
||||
emit('reloadTrade', item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -36,7 +44,7 @@ const cancelOpenOrderHandler = (item: Trade) => {
|
|||
title="Actions"
|
||||
@click="popoverOpen = !popoverOpen"
|
||||
>
|
||||
<ActionIcon :size="16" title="Actions" />
|
||||
<i-mdi-gesture-tap />
|
||||
</b-button>
|
||||
<b-popover
|
||||
:target="`btn-actions-${id}`"
|
||||
|
@ -55,9 +63,10 @@ const cancelOpenOrderHandler = (item: Trade) => {
|
|||
$emit('deleteTrade', trade);
|
||||
"
|
||||
@cancel-open-order="cancelOpenOrderHandler"
|
||||
@reload-trade="handleReloadTrade"
|
||||
/>
|
||||
<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-popover>
|
||||
</div>
|
||||
|
|
|
@ -128,25 +128,16 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { formatPercent, formatPriceCurrency, formatPrice, timestampms } from '@/shared/formatters';
|
||||
import ValuePair from '@/components/general/ValuePair.vue';
|
||||
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
import { Trade } from '@/types';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
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 };
|
||||
},
|
||||
defineProps({
|
||||
trade: { required: true, type: Object as () => Trade },
|
||||
stakeCurrency: { required: true, type: String },
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
@force-exit="forceExitHandler"
|
||||
@force-exit-partial="forceExitPartialHandler"
|
||||
@cancel-open-order="cancelOpenOrderHandler"
|
||||
@reload-trade="reloadTradeHandler"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(pair)="row">
|
||||
|
@ -76,14 +77,9 @@
|
|||
:per-page="perPage"
|
||||
aria-controls="my-table"
|
||||
></b-pagination>
|
||||
<b-form-input
|
||||
v-if="showFilter"
|
||||
v-model="filterText"
|
||||
type="text"
|
||||
placeholder="Filter"
|
||||
size="sm"
|
||||
style="width: unset"
|
||||
/>
|
||||
<b-form-group v-if="showFilter" label-for="trade-filter">
|
||||
<b-form-input id="trade-filter" v-model="filterText" type="text" placeholder="Filter" />
|
||||
</b-form-group>
|
||||
</div>
|
||||
<force-exit-form v-if="activeTrades" v-model="forceExitVisible" :trade="feTrade" />
|
||||
<b-modal v-model="removeTradeVisible" title="Exit trade" @ok="forceExitExecuter">
|
||||
|
@ -246,6 +242,10 @@ const cancelOpenOrderHandler = (item: Trade) => {
|
|||
removeTradeVisible.value = true;
|
||||
};
|
||||
|
||||
function reloadTradeHandler(item: Trade) {
|
||||
botStore.reloadTradeMulti({ tradeid: String(item.trade_id), botId: item.botId });
|
||||
}
|
||||
|
||||
const handleContextMenuEvent = (item, index, event) => {
|
||||
// stop browser context menu from appearing
|
||||
if (!props.activeTrades) {
|
||||
|
|
|
@ -2,40 +2,34 @@
|
|||
<span :title="timezoneTooltip">{{ formattedDate }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
|
||||
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DateTimeTZ',
|
||||
props: {
|
||||
date: { required: true, type: Number },
|
||||
showTimezone: { required: false, type: Boolean, default: false },
|
||||
dateOnly: { required: false, type: Boolean, default: false },
|
||||
},
|
||||
setup(props) {
|
||||
const formattedDate = computed((): string => {
|
||||
if (props.dateOnly) {
|
||||
return timestampToDateString(props.date);
|
||||
}
|
||||
if (props.showTimezone) {
|
||||
return timestampmsWithTimezone(props.date);
|
||||
}
|
||||
return timestampms(props.date);
|
||||
});
|
||||
const props = defineProps({
|
||||
date: { required: true, type: Number },
|
||||
showTimezone: { required: false, type: Boolean, default: false },
|
||||
dateOnly: { required: false, type: Boolean, default: false },
|
||||
});
|
||||
const formattedDate = computed((): string => {
|
||||
if (props.dateOnly) {
|
||||
return timestampToDateString(props.date);
|
||||
}
|
||||
if (props.showTimezone) {
|
||||
return timestampmsWithTimezone(props.date);
|
||||
}
|
||||
return timestampms(props.date);
|
||||
});
|
||||
|
||||
const timezoneTooltip = computed((): string => {
|
||||
const time1 = timestampmsWithTimezone(props.date);
|
||||
const timeUTC = timestampmsWithTimezone(props.date, 'UTC');
|
||||
if (time1 === timeUTC) {
|
||||
return timeUTC;
|
||||
}
|
||||
const timezoneTooltip = computed((): string => {
|
||||
const time1 = timestampmsWithTimezone(props.date);
|
||||
const timeUTC = timestampmsWithTimezone(props.date, 'UTC');
|
||||
if (time1 === timeUTC) {
|
||||
return timeUTC;
|
||||
}
|
||||
|
||||
return `${time1}\n${timeUTC}`;
|
||||
});
|
||||
return { formattedDate, timezoneTooltip };
|
||||
},
|
||||
return `${time1}\n${timeUTC}`;
|
||||
});
|
||||
</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>
|
||||
<div :title="hint">
|
||||
<InfoIcon :size="18" />
|
||||
<i-mdi-information-outline />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InfoIcon from 'vue-material-design-icons/InformationOutline.vue';
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
hint: { type: String, required: true },
|
||||
});
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import ValuePair from '@/components/general/ValuePair.vue';
|
||||
|
||||
it('renders a message', () => {
|
||||
const msg = 'Test description';
|
||||
cy.mount(ValuePair, { props: { description: msg } });
|
||||
|
||||
cy.get('label').contains(msg);
|
||||
describe('ValuePair.vue', () => {
|
||||
it('Renders a message', () => {
|
||||
const msg = 'Test description';
|
||||
// https://github.com/cypress-io/cypress/issues/26628
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
cy.mount(ValuePair, { props: { description: msg } });
|
||||
cy.get('label').contains(msg);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,20 +10,14 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import InfoBox from '@/components/general/InfoBox.vue';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ValuePair',
|
||||
components: { InfoBox },
|
||||
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' },
|
||||
},
|
||||
defineProps({
|
||||
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>
|
||||
|
||||
|
|
|
@ -3,49 +3,54 @@
|
|||
<!-- Only visible on xs (phone) viewport! -->
|
||||
<hr class="my-0" />
|
||||
<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">
|
||||
<OpenTradesIcon />
|
||||
<router-link
|
||||
v-if="!botStore.canRunBacktest"
|
||||
class="nav-link navbar-nav align-items-center"
|
||||
to="/open_trades"
|
||||
>
|
||||
<i-mdi-folder-open height="24" width="24" />
|
||||
Trades
|
||||
</router-link>
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/trade_history">
|
||||
<ClosedTradesIcon />
|
||||
<router-link
|
||||
v-if="!botStore.canRunBacktest"
|
||||
class="nav-link navbar-nav align-items-center"
|
||||
to="/trade_history"
|
||||
>
|
||||
<i-mdi-folder-lock height="24" width="24" />
|
||||
History
|
||||
</router-link>
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/pairlist">
|
||||
<PairListIcon />
|
||||
<router-link
|
||||
v-if="!botStore.canRunBacktest"
|
||||
class="nav-link navbar-nav align-items-center"
|
||||
to="/pairlist"
|
||||
>
|
||||
<i-mdi-view-list height="24" width="24" />
|
||||
Pairlist
|
||||
</router-link>
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/balance">
|
||||
<BalanceIcon />
|
||||
<router-link
|
||||
v-if="!botStore.canRunBacktest"
|
||||
class="nav-link navbar-nav align-items-center"
|
||||
to="/balance"
|
||||
>
|
||||
<i-mdi-bank height="24" width="24" />
|
||||
Balance
|
||||
</router-link>
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/dashboard">
|
||||
<DashboardIcon />
|
||||
<router-link
|
||||
v-if="!botStore.canRunBacktest"
|
||||
class="nav-link navbar-nav align-items-center"
|
||||
to="/dashboard"
|
||||
>
|
||||
<i-mdi-view-dashboard-outline height="24" width="24" />
|
||||
Dashboard
|
||||
</router-link>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script 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';
|
||||
<script setup lang="ts">
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NavFooter',
|
||||
components: { OpenTradesIcon, ClosedTradesIcon, BalanceIcon, PairListIcon, DashboardIcon },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
return {
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
const botStore = useBotStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,7 +2,8 @@ import { createPinia, PiniaVuePlugin } from 'pinia';
|
|||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
import { createApp } from '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 router from './router';
|
||||
|
||||
|
@ -14,7 +15,6 @@ pinia.use(piniaPluginPersistedstate);
|
|||
myApp.use(pinia);
|
||||
|
||||
myApp.use(router);
|
||||
myApp.use(BootstrapVue3);
|
||||
myApp.use(GridLayout);
|
||||
|
||||
// Vue.config.productionTip = false;
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import BootstrapVue3 from 'bootstrap-vue-next';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
|
||||
|
||||
import '@/styles/main.scss';
|
||||
|
||||
export { BootstrapVue3 };
|
||||
|
|
|
@ -18,7 +18,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||
{
|
||||
path: '/graph',
|
||||
name: 'Freqtrade Graph',
|
||||
component: () => import('@/views/GraphsView.vue'),
|
||||
component: () => import('@/views/ChartsView.vue'),
|
||||
},
|
||||
{
|
||||
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 closeIdx = columns.indexOf('close');
|
||||
const highIdx = columns.indexOf('high');
|
||||
|
@ -26,3 +26,5 @@ export default function heikinAshiDataset(columns: string[], data: Array<number[
|
|||
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 {
|
||||
const allInfo = UserService.getAllLoginInfos();
|
||||
const response: BotDescriptors = {};
|
||||
Object.entries(allInfo).forEach(([k, v], idx) => {
|
||||
response[k] = {
|
||||
botId: k,
|
||||
botName: v.botName,
|
||||
botUrl: v.apiUrl,
|
||||
sortId: v.sortId ?? idx,
|
||||
};
|
||||
});
|
||||
Object.keys(allInfo)
|
||||
.sort((a, b) => (allInfo[a].sortId ?? 0) - (allInfo[b].sortId ?? 0))
|
||||
.forEach((k, idx) => {
|
||||
response[k] = {
|
||||
botId: k,
|
||||
botName: allInfo[k].botName,
|
||||
botUrl: allInfo[k].apiUrl,
|
||||
sortId: allInfo[k].sortId ?? idx,
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
@ -520,7 +520,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
},
|
||||
async getState() {
|
||||
try {
|
||||
const { data } = await api.get('/show_config');
|
||||
const { data } = await api.get<BotState>('/show_config');
|
||||
this.botState = data;
|
||||
this.botStatusAvailable = true;
|
||||
this.startWebSocket();
|
||||
|
@ -654,6 +654,18 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
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() {
|
||||
try {
|
||||
const res = await api.post('/start_trade', {});
|
||||
|
@ -692,10 +704,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
showAlert(
|
||||
`Error occured entering: '${(error as any).response?.data?.error}'`,
|
||||
'danger',
|
||||
);
|
||||
showAlert(`Error occured entering: '${error.response?.data?.error}'`, 'danger');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
@ -730,9 +739,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
showAlert(
|
||||
`Error occured while adding pairs to Blacklist: '${
|
||||
(error as any).response?.data?.error
|
||||
}'`,
|
||||
`Error occured while adding pairs to Blacklist: '${error.response?.data?.error}'`,
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
|
@ -778,9 +785,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
showAlert(
|
||||
`Error occured while removing pairs from Blacklist: '${
|
||||
(error as any).response?.data?.error
|
||||
}'`,
|
||||
`Error occured while removing pairs from Blacklist: '${error.response?.data?.error}'`,
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
|
@ -876,6 +881,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
_handleWebsocketMessage(ws, event: MessageEvent<any>) {
|
||||
const msg: FTWsMessage = JSON.parse(event.data);
|
||||
switch (msg.type) {
|
||||
|
@ -899,6 +905,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
}
|
||||
default:
|
||||
// Unhandled events ...
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
console.log(`Received event ${(msg as any).type}`);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@ import {
|
|||
DailyPayload,
|
||||
DailyRecord,
|
||||
DailyReturnValue,
|
||||
MultiCancelOpenOrderPayload,
|
||||
MultiDeletePayload,
|
||||
MultiForcesellPayload,
|
||||
MultiReloadTradePayload,
|
||||
ProfitInterface,
|
||||
Trade,
|
||||
} from '@/types';
|
||||
|
@ -234,7 +236,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
// Ensure all bots status is correct.
|
||||
await this.pingAll();
|
||||
|
||||
const botStoreUpdates: Promise<any>[] = [];
|
||||
const botStoreUpdates: Promise<BotState>[] = [];
|
||||
this.allBotStores.forEach((bot) => {
|
||||
if (bot.isBotOnline && !bot.botStatusAvailable) {
|
||||
botStoreUpdates.push(bot.getState());
|
||||
|
@ -283,7 +285,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
},
|
||||
async pingAll() {
|
||||
await Promise.all(
|
||||
Object.entries(this.botStores).map(async ([_, v]) => {
|
||||
Object.values(this.botStores).map(async (v) => {
|
||||
try {
|
||||
await v.fetchPing();
|
||||
} catch {
|
||||
|
@ -293,7 +295,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
);
|
||||
},
|
||||
allGetState() {
|
||||
Object.entries(this.botStores).map(async ([_, v]) => {
|
||||
Object.values(this.botStores).map(async (v) => {
|
||||
try {
|
||||
await v.getState();
|
||||
} catch {
|
||||
|
@ -317,9 +319,12 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
async deleteTradeMulti(deletePayload: MultiDeletePayload) {
|
||||
return this.botStores[deletePayload.botId].deleteTrade(deletePayload.tradeid);
|
||||
},
|
||||
async cancelOpenOrderMulti(deletePayload: MultiDeletePayload) {
|
||||
async cancelOpenOrderMulti(deletePayload: MultiCancelOpenOrderPayload) {
|
||||
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 {
|
||||
getAllPlotConfigNames,
|
||||
getCustomPlotConfig,
|
||||
getPlotConfigName,
|
||||
storeCustomPlotConfig,
|
||||
storePlotConfigName,
|
||||
} from '@/shared/storage';
|
||||
import { PlotConfigStorage, EMPTY_PLOTCONFIG, PlotConfig } from '@/types';
|
||||
import { deepClone } from '@/shared/deepClone';
|
||||
import { EMPTY_PLOTCONFIG, PlotConfig, PlotConfigStorage } from '@/types';
|
||||
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', {
|
||||
state: () => {
|
||||
return {
|
||||
customPlotConfig: {} as PlotConfigStorage,
|
||||
plotConfigName: getPlotConfigName(),
|
||||
availablePlotConfigNames: getAllPlotConfigNames(),
|
||||
plotConfig: { ...EMPTY_PLOTCONFIG },
|
||||
customPlotConfigs: {} as PlotConfigStorage,
|
||||
plotConfigName: 'default',
|
||||
isEditing: false,
|
||||
editablePlotConfig: { ...EMPTY_PLOTCONFIG } as PlotConfig,
|
||||
};
|
||||
},
|
||||
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 },
|
||||
},
|
||||
actions: {
|
||||
saveCustomPlotConfig(plotConfig: PlotConfigStorage) {
|
||||
this.customPlotConfig = plotConfig;
|
||||
storeCustomPlotConfig(plotConfig);
|
||||
this.availablePlotConfigNames = getAllPlotConfigNames();
|
||||
saveCustomPlotConfig(name: string, plotConfig: PlotConfig) {
|
||||
// This will autosave to storage due to pinia-persist
|
||||
this.customPlotConfigs[name] = plotConfig;
|
||||
},
|
||||
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;
|
||||
storePlotConfigName(plotConfigName);
|
||||
},
|
||||
|
||||
plotConfigChanged(plotConfigName = '') {
|
||||
console.log('plotConfigChanged');
|
||||
this.setPlotConfigName(plotConfigName ? plotConfigName : this.plotConfigName);
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
|
||||
},
|
||||
setPlotConfig(plotConfig: PlotConfig) {
|
||||
console.log('emit...');
|
||||
this.plotConfig = { ...plotConfig };
|
||||
if (plotConfigName) {
|
||||
this.plotConfigName = plotConfigName;
|
||||
}
|
||||
console.log('plotConfigChanged', this.plotConfigName);
|
||||
if (this.isEditing) {
|
||||
this.editablePlotConfig = deepClone(this.customPlotConfigs[this.plotConfigName]);
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
key: FT_PLOT_CONFIG_KEY,
|
||||
paths: ['plotConfigName', 'customPlotConfigs'],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
|
||||
.v-select * {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
|
@ -22,13 +22,21 @@
|
|||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #ffffff
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.text-bg-primary {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.vs__open-indicator {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
$bg-dark: rgb(18, 18, 18);
|
||||
|
||||
|
@ -133,8 +141,9 @@
|
|||
}
|
||||
|
||||
// Styles for searchable select
|
||||
.vs__dropdown-toggle {
|
||||
.vs__dropdown-toggle, .vs__clear {
|
||||
border-color: lighten($bg-dark, 20%);
|
||||
color: $fg-color;
|
||||
// border: 1px solid $fg-color;
|
||||
}
|
||||
|
||||
|
@ -189,7 +198,11 @@
|
|||
.form-select {
|
||||
color: $fg-color;
|
||||
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 {
|
||||
|
|
|
@ -50,14 +50,32 @@ export interface ExitReasonResults {
|
|||
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 {
|
||||
trades: ClosedTrade[];
|
||||
locks: Lock[];
|
||||
best_pair: PairResult;
|
||||
worst_pair: PairResult;
|
||||
results_per_pair: Array<PairResult>;
|
||||
sell_reason_summary?: Array<ExitReasonResults>;
|
||||
exit_reason_summary?: Array<ExitReasonResults>;
|
||||
results_per_pair: PairResult[];
|
||||
sell_reason_summary?: ExitReasonResults[];
|
||||
exit_reason_summary?: ExitReasonResults[];
|
||||
periodic_breakdown?: PeriodicBreakdown;
|
||||
left_open_trades: Trade[];
|
||||
total_trades: number;
|
||||
total_volume: number;
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
export interface BalanceRecords {
|
||||
[key: string]: string | number;
|
||||
balance: number;
|
||||
currency: string;
|
||||
est_stake: number;
|
||||
est_stake_bot?: number;
|
||||
free: number;
|
||||
used: number;
|
||||
bot_owned?: number;
|
||||
stake: string;
|
||||
// Properties added in v 2.x
|
||||
// Temporarily disabled to fix type errors
|
||||
// side: string;
|
||||
// leverage: number;
|
||||
// is_position: boolean;
|
||||
// position: number;
|
||||
side: string;
|
||||
leverage: number;
|
||||
is_position: boolean;
|
||||
position: number;
|
||||
is_bot_managed?: boolean;
|
||||
}
|
||||
|
||||
export interface BalanceInterface {
|
||||
|
@ -21,10 +22,14 @@ export interface BalanceInterface {
|
|||
stake: string;
|
||||
/** Fiat symbol used */
|
||||
symbol: string;
|
||||
/** Total Balance in stake currency */
|
||||
/** Total Account Balance in stake currency */
|
||||
total: number;
|
||||
/** Balance in FIAT currency */
|
||||
/** Total Bot Balance in stake currency */
|
||||
total_bot?: number;
|
||||
/** Account Balance in FIAT currency */
|
||||
value: number;
|
||||
/** Bot Balance in FIAT currency */
|
||||
value_bot?: number;
|
||||
/** Assumed starting capital */
|
||||
starting_capital: number;
|
||||
/** Change between starting capital and current value */
|
||||
|
@ -36,3 +41,13 @@ export interface BalanceInterface {
|
|||
starting_capital_fiat_ratio: 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;
|
||||
}
|
||||
|
||||
export type CumProfitChartData = {
|
||||
date: number;
|
||||
profit?: number;
|
||||
currentProfit?: number;
|
||||
};
|
||||
|
||||
export interface ChartSliderPosition {
|
||||
startValue: number;
|
||||
endValue: number | undefined;
|
||||
|
|
|
@ -7,6 +7,7 @@ export enum ChartType {
|
|||
export interface IndicatorConfig {
|
||||
color?: string;
|
||||
type?: ChartType;
|
||||
fill_to?: string;
|
||||
}
|
||||
|
||||
export interface PlotConfig {
|
||||
|
|
|
@ -26,8 +26,10 @@ export interface ProfitInterface {
|
|||
trade_count: number;
|
||||
closed_trade_count: number;
|
||||
first_trade_date: string;
|
||||
first_trade_humanized: string;
|
||||
first_trade_timestamp: number;
|
||||
latest_trade_date: string;
|
||||
latest_trade_humanized: string;
|
||||
latest_trade_timestamp: number;
|
||||
avg_duration: string;
|
||||
best_pair: string;
|
||||
|
|
|
@ -134,7 +134,7 @@ export interface ClosedTrade extends TradeBase {
|
|||
fee_close_cost?: number;
|
||||
fee_close_currency?: string;
|
||||
|
||||
sell_reason: string;
|
||||
exit_reason: string;
|
||||
min_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. */
|
||||
export interface MultiDeletePayload {
|
||||
export interface MultiBotIdPayload {
|
||||
tradeid: string;
|
||||
botId: string;
|
||||
}
|
||||
|
||||
export type MultiDeletePayload = MultiBotIdPayload;
|
||||
export type MultiReloadTradePayload = MultiBotIdPayload;
|
||||
export type MultiCancelOpenOrderPayload = MultiBotIdPayload;
|
||||
|
||||
export interface PerformanceEntry {
|
||||
count: number;
|
||||
pair: string;
|
||||
|
@ -121,8 +125,7 @@ export interface EntryPricing extends PriceBase {
|
|||
export interface BotState {
|
||||
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;
|
||||
/** Futures, margin or spot */
|
||||
trading_mode?: TradingMode;
|
||||
|
@ -136,8 +139,6 @@ export interface BotState {
|
|||
unfilledtimeout: UnfilledTimeout;
|
||||
order_types: OrderTypes;
|
||||
exchange: string;
|
||||
/** @deprecated replaced by force_entry_enable in 2.x */
|
||||
forcebuy_enabled?: boolean;
|
||||
force_entry_enable?: boolean;
|
||||
max_open_trades: number;
|
||||
minimal_roi: object;
|
||||
|
@ -206,6 +207,7 @@ export interface PairHistoryPayload {
|
|||
timeframe: string;
|
||||
timerange: string;
|
||||
strategy: string;
|
||||
freqaimodel?: string;
|
||||
}
|
||||
|
||||
export interface PairHistory {
|
||||
|
@ -214,7 +216,7 @@ export interface PairHistory {
|
|||
timeframe: string;
|
||||
timeframe_ms: number;
|
||||
columns: string[];
|
||||
data: Array<number[]>;
|
||||
data: number[][];
|
||||
length: number;
|
||||
/** Number of buy signals in this response */
|
||||
buy_signals: number;
|
||||
|
|
|
@ -316,6 +316,7 @@
|
|||
:timerange="timerange"
|
||||
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
:freqai-model="freqAI.enabled ? freqAI.model : undefined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -86,7 +86,11 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Cumulative Profit">
|
||||
<CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
|
||||
<CumProfitChart
|
||||
:trades="botStore.allTradesSelectedBots"
|
||||
:open-trades="botStore.allOpenTradesSelectedBots"
|
||||
:show-title="false"
|
||||
/>
|
||||
</DraggableContainer>
|
||||
</grid-item>
|
||||
<grid-item
|
||||
|
|
|
@ -8,7 +8,3 @@
|
|||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -4,14 +4,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import LogViewer from '@/components/ftbot/LogViewer.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LogView',
|
||||
components: { LogViewer },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-button @click="openLoginModal()"
|
||||
><LoginIcon :size="16" class="me-1" />{{ loginText }}</b-button
|
||||
>
|
||||
<b-button @click="openLoginModal()"><i-mdi-login class="me-1" />{{ loginText }}</b-button>
|
||||
<b-modal
|
||||
id="modal-prevent-closing"
|
||||
v-model="loginViewOpen"
|
||||
|
@ -23,7 +21,6 @@
|
|||
import Login from '@/components/BotLogin.vue';
|
||||
import { AuthStorageWithBotId } from '@/types';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import LoginIcon from 'vue-material-design-icons/Login.vue';
|
||||
|
||||
defineProps({
|
||||
loginText: { required: false, default: 'Login', type: String },
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<b-card header="Freqtrade bot Login">
|
||||
<Login ref="loginForm" />
|
||||
<BotLogin ref="loginForm" />
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Login from '@/components/BotLogin.vue';
|
||||
import BotLogin from '@/components/BotLogin.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
size="sm"
|
||||
class="align-self-start mt-1 ms-1"
|
||||
@click="botStore.activeBot.setDetailTrade(null)"
|
||||
><BackIcon /> Back</b-button
|
||||
><i-mdi-arrow-left /> Back</b-button
|
||||
>
|
||||
<TradeDetail
|
||||
:trade="botStore.activeBot.tradeDetail"
|
||||
|
@ -40,31 +40,15 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import CustomTradeList from '@/components/ftbot/CustomTradeList.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';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TradesList',
|
||||
components: {
|
||||
CustomTradeList,
|
||||
TradeDetail,
|
||||
BackIcon,
|
||||
},
|
||||
props: {
|
||||
history: { default: false, type: Boolean },
|
||||
},
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
return {
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
defineProps({
|
||||
history: { default: false, type: Boolean },
|
||||
});
|
||||
const botStore = useBotStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"types": [
|
||||
"cypress",
|
||||
"vite/client",
|
||||
"unplugin-icons/types/vue",
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import createVuePlugin from '@vitejs/plugin-vue';
|
||||
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/
|
||||
export default defineConfig({
|
||||
plugins: [createVuePlugin({})],
|
||||
plugins: [
|
||||
createVuePlugin({}),
|
||||
Components({
|
||||
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
|
||||
dirs: [],
|
||||
dts: true,
|
||||
}),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
dedupe: ['vue'],
|
||||
alias: {
|
||||
|
@ -29,6 +43,7 @@ export default defineConfig({
|
|||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
host: '127.0.0.1',
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user