mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-21 23:53:52 +00:00
Merge pull request #737 from freqtrade/pinia
Vuex to Pinia and composition API
This commit is contained in:
commit
30317bf2b0
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,6 +3,8 @@ node_modules
|
|||
package-lock.json
|
||||
/dist
|
||||
|
||||
cypress/screenshots/*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
|
|
@ -81,6 +81,14 @@ describe('Login', () => {
|
|||
cy.get('span').should('contain', 'TestBot');
|
||||
// Check API calls have been made.
|
||||
cy.wait('@RandomAPICall');
|
||||
// login button gone
|
||||
cy.get('button').should('not.contain', 'Login');
|
||||
|
||||
// Test logout
|
||||
cy.get('[id=avatar-drop]').parent().click();
|
||||
cy.get('.dropdown-menu > a:last').click();
|
||||
cy.get('button').should('contain', 'Login');
|
||||
// login button there again
|
||||
});
|
||||
|
||||
it('Test Login failed - wrong api url', () => {
|
||||
|
|
11
package.json
11
package.json
|
@ -27,21 +27,21 @@
|
|||
"echarts": "^5.3.2",
|
||||
"favico.js": "^0.3.10",
|
||||
"humanize-duration": "^3.27.1",
|
||||
"pinia": "^2.0.13",
|
||||
"pinia-plugin-persistedstate": "^1.5.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-demi": "0.12.5",
|
||||
"vue-echarts": "^6.0.2",
|
||||
"vue-grid-layout": "^2.3.12",
|
||||
"vue-material-design-icons": "^5.0.0",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-select": "^3.18.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-composition-helpers": "^1.1.0"
|
||||
"vue2-helpers": "^1.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/vue": "^2.2.3",
|
||||
"@cypress/vite-dev-server": "^2.2.2",
|
||||
"@cypress/vue": "^2.2.3",
|
||||
"@types/echarts": "^4.9.14",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
|
@ -65,7 +65,6 @@
|
|||
"vite": "^2.9.5",
|
||||
"vite-jest": "^0.1.4",
|
||||
"vite-plugin-vue2": "^1.5.1",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuex-class": "^0.3.2"
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
}
|
||||
|
|
27
src/App.vue
27
src/App.vue
|
@ -7,27 +7,24 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import NavBar from '@/components/layout/NavBar.vue';
|
||||
import NavFooter from '@/components/layout/NavFooter.vue';
|
||||
import Body from '@/components/layout/Body.vue';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { SettingsGetters } from './store/modules/settings';
|
||||
import { setTimezone } from './shared/formatters';
|
||||
import StoreModules from './store/storeSubModules';
|
||||
import { defineComponent, onMounted } from '@vue/composition-api';
|
||||
import { useSettingsStore } from './stores/settings';
|
||||
|
||||
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: { NavBar, Body, NavFooter },
|
||||
})
|
||||
export default class App extends Vue {
|
||||
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
||||
|
||||
mounted() {
|
||||
setTimezone(this.timezone);
|
||||
}
|
||||
}
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore();
|
||||
onMounted(() => {
|
||||
setTimezone(settingsStore.timezone);
|
||||
});
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -31,14 +31,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent, ref, onMounted } from '@vue/composition-api';
|
||||
import axios from 'axios';
|
||||
import ThemeLightDark from 'vue-material-design-icons/Brightness6.vue';
|
||||
import { themeList } from '@/shared/themes';
|
||||
import { mapActions } from 'vuex';
|
||||
import { FTHTMLStyleElement } from '@/types/styleElement';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
name: 'BootswatchThemeSelect',
|
||||
components: { ThemeLightDark },
|
||||
props: {
|
||||
|
@ -47,27 +46,14 @@ export default Vue.extend({
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTheme: '',
|
||||
themeList,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// If a theme has been stored in localstorage, the theme will be set.
|
||||
if (window.localStorage.theme) this.setTheme(window.localStorage.theme);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setCurrentTheme']),
|
||||
handleClick(e) {
|
||||
this.setTheme(e.target.name.trim());
|
||||
},
|
||||
toggleNight() {
|
||||
this.setTheme(this.activeTheme === 'bootstrap' ? 'bootstrap_dark' : 'bootstrap');
|
||||
},
|
||||
setTheme(themeName) {
|
||||
setup(props) {
|
||||
const activeTheme = ref('');
|
||||
const themeList = ref([]);
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const setTheme = (themeName) => {
|
||||
// If theme is already active, do nothing.
|
||||
if (this.activeTheme === themeName) {
|
||||
if (activeTheme.value === themeName) {
|
||||
return;
|
||||
}
|
||||
if (themeName.toLowerCase() === 'bootstrap' || themeName.toLowerCase() === 'bootstrap_dark') {
|
||||
|
@ -81,7 +67,7 @@ export default Vue.extend({
|
|||
bw.forEach((style, index) => {
|
||||
(bw[index] as FTHTMLStyleElement).disabled = true;
|
||||
});
|
||||
if (this.simple && this.activeTheme) {
|
||||
if (props.simple && activeTheme.value) {
|
||||
// Only transition if simple mode is active
|
||||
document.documentElement.classList.add('ft-theme-transition');
|
||||
window.setTimeout(() => {
|
||||
|
@ -110,10 +96,21 @@ export default Vue.extend({
|
|||
});
|
||||
}
|
||||
// Save the theme as localstorage
|
||||
this.setCurrentTheme(themeName);
|
||||
this.activeTheme = themeName;
|
||||
},
|
||||
fetchApi() {
|
||||
settingsStore.currentTheme = themeName;
|
||||
activeTheme.value = themeName;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (window.localStorage.theme) setTheme(window.localStorage.theme);
|
||||
});
|
||||
|
||||
const handleClick = (e) => {
|
||||
setTheme(e.target.name.trim());
|
||||
};
|
||||
const toggleNight = () => {
|
||||
setTheme(activeTheme.value === 'bootstrap' ? 'bootstrap_dark' : 'bootstrap');
|
||||
};
|
||||
const fetchApi = () => {
|
||||
// Fetches boostswatch api and dynamically sets themes.
|
||||
// Not used, but useful for updating the static array of themes if bootswatch dependency is outdated.
|
||||
axios
|
||||
|
@ -132,7 +129,15 @@ export default Vue.extend({
|
|||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
};
|
||||
return {
|
||||
activeTheme,
|
||||
themeList,
|
||||
setTheme,
|
||||
handleClick,
|
||||
toggleNight,
|
||||
fetchApi,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div class="align-items-center d-flex">
|
||||
<span class="ml-2 mr-1 align-middle">{{
|
||||
allIsBotOnline[bot.botId] ? '🟢' : '🔴'
|
||||
botStore.botStores[bot.botId].isBotOnline ? '🟢' : '🔴'
|
||||
}}</span>
|
||||
<b-form-checkbox
|
||||
v-model="autoRefreshLoc"
|
||||
|
@ -29,58 +29,55 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import EditIcon from 'vue-material-design-icons/Pencil.vue';
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import { BotDescriptor, BotDescriptors } from '@/types';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { BotDescriptor } from '@/types';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'BotEntry',
|
||||
components: {
|
||||
DeleteIcon,
|
||||
EditIcon,
|
||||
},
|
||||
})
|
||||
export default class BotList extends Vue {
|
||||
@Prop({ default: false, type: Object }) bot!: BotDescriptor;
|
||||
props: {
|
||||
bot: { required: true, type: Object },
|
||||
noButtons: { default: false, type: Boolean },
|
||||
},
|
||||
emits: ['edit'],
|
||||
setup(props, { root }) {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Prop({ default: false, type: Boolean }) noButtons!: boolean;
|
||||
const changeEvent = (v) => {
|
||||
botStore.botStores[props.bot.botId].setAutoRefresh(v);
|
||||
};
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allIsBotOnline];
|
||||
const clickRemoveBot = (bot: BotDescriptor) => {
|
||||
//
|
||||
root.$bvModal
|
||||
.msgBoxConfirm(`Really remove (logout) from '${bot.botName}' (${bot.botId})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
botStore.removeBot(bot.botId);
|
||||
}
|
||||
});
|
||||
};
|
||||
const autoRefreshLoc = computed({
|
||||
get() {
|
||||
return botStore.botStores[props.bot.botId].autoRefresh;
|
||||
},
|
||||
set() {
|
||||
// pass
|
||||
},
|
||||
});
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAutoRefresh];
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAvailableBots]: BotDescriptors;
|
||||
|
||||
@ftbot.Action removeBot;
|
||||
|
||||
@ftbot.Action selectBot;
|
||||
|
||||
get autoRefreshLoc() {
|
||||
return this.allAutoRefresh[this.bot.botId];
|
||||
}
|
||||
|
||||
set autoRefreshLoc(v) {
|
||||
// Dummy setter - Set via change event to avoid bouncing
|
||||
}
|
||||
|
||||
changeEvent(v) {
|
||||
this.$store.dispatch(`ftbot/${this.bot.botId}/setAutoRefresh`, v);
|
||||
}
|
||||
|
||||
clickRemoveBot(bot: BotDescriptor) {
|
||||
//
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(`Really remove (logout) from '${bot.botName}' (${bot.botId})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
this.removeBot(bot.botId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
changeEvent,
|
||||
clickRemoveBot,
|
||||
autoRefreshLoc,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div v-if="botCount > 0">
|
||||
<div v-if="botStore.botCount > 0">
|
||||
<h3 v-if="!small">Available bots</h3>
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="bot in allAvailableBots"
|
||||
v-for="bot in botStore.availableBots"
|
||||
:key="bot.botId"
|
||||
:active="bot.botId === selectedBot"
|
||||
:active="bot.botId === botStore.selectedBot"
|
||||
button
|
||||
:title="`${bot.botId} - ${bot.botName} - ${bot.botUrl}`"
|
||||
@click="selectBot(bot.botId)"
|
||||
@click="botStore.selectBot(bot.botId)"
|
||||
>
|
||||
<bot-rename
|
||||
v-if="editingBots.includes(bot.botId)"
|
||||
|
@ -25,51 +25,44 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import LoginModal from '@/views/LoginModal.vue';
|
||||
import BotEntry from '@/components/BotEntry.vue';
|
||||
import BotRename from '@/components/BotRename.vue';
|
||||
import { BotDescriptors } from '@/types';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
LoginModal,
|
||||
BotEntry,
|
||||
BotRename,
|
||||
export default defineComponent({
|
||||
name: 'BotList',
|
||||
components: { LoginModal, BotEntry, BotRename },
|
||||
props: {
|
||||
small: { default: false, type: Boolean },
|
||||
},
|
||||
})
|
||||
export default class BotList extends Vue {
|
||||
@Prop({ default: false, type: Boolean }) small!: boolean;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.botCount]: number;
|
||||
const editingBots = ref<string[]>([]);
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.selectedBot]: string;
|
||||
const editBot = (botId: string) => {
|
||||
if (!editingBots.value.includes(botId)) {
|
||||
editingBots.value.push(botId);
|
||||
}
|
||||
};
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allIsBotOnline]: Record<string, boolean>;
|
||||
const stopEditBot = (botId: string) => {
|
||||
if (!editingBots.value.includes(botId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAvailableBots]: BotDescriptors;
|
||||
editingBots.value.splice(editingBots.value.indexOf(botId), 1);
|
||||
};
|
||||
|
||||
@ftbot.Action selectBot;
|
||||
|
||||
editingBots: string[] = [];
|
||||
|
||||
editBot(botId: string) {
|
||||
if (!this.editingBots.includes(botId)) {
|
||||
this.editingBots.push(botId);
|
||||
}
|
||||
}
|
||||
|
||||
stopEditBot(botId: string) {
|
||||
if (!this.editingBots.includes(botId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editingBots.splice(this.editingBots.indexOf(botId), 1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
editingBots,
|
||||
editBot,
|
||||
stopEditBot,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -22,36 +22,39 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import CheckIcon from 'vue-material-design-icons/Check.vue';
|
||||
import CloseIcon from 'vue-material-design-icons/Close.vue';
|
||||
import { BotDescriptor, RenameBotPayload } from '@/types';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { BotDescriptor } from '@/types';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'BotRename',
|
||||
components: {
|
||||
CheckIcon,
|
||||
CloseIcon,
|
||||
},
|
||||
})
|
||||
export default class BotList extends Vue {
|
||||
@Prop({ required: true, type: Object }) bot!: BotDescriptor;
|
||||
props: {
|
||||
bot: { type: Object as () => BotDescriptor, required: true },
|
||||
},
|
||||
emits: ['saved'],
|
||||
setup(props, { emit }) {
|
||||
const botStore = useBotStore();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action renameBot!: (payload: RenameBotPayload) => Promise<void>;
|
||||
const newName = ref<string>(props.bot.botName);
|
||||
|
||||
newName: string = this.bot.botName;
|
||||
const save = () => {
|
||||
botStore.renameBot({
|
||||
botId: props.bot.botId,
|
||||
botName: newName.value,
|
||||
});
|
||||
|
||||
save() {
|
||||
this.renameBot({
|
||||
botId: this.bot.botId,
|
||||
botName: this.newName,
|
||||
});
|
||||
|
||||
this.$emit('saved');
|
||||
}
|
||||
}
|
||||
emit('saved');
|
||||
};
|
||||
return {
|
||||
newName,
|
||||
save,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<form ref="form" novalidate @submit.stop.prevent="handleSubmit" @reset="handleReset">
|
||||
<form ref="formRef" novalidate @submit.stop.prevent="handleSubmit" @reset="handleReset">
|
||||
<b-form-group label="Bot Name" label-for="name-input">
|
||||
<b-form-input
|
||||
id="name-input"
|
||||
|
@ -72,146 +72,146 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Emit, Prop } from 'vue-property-decorator';
|
||||
import { Action, namespace } from 'vuex-class';
|
||||
import { useUserService } from '@/shared/userService';
|
||||
|
||||
import { AuthPayload, BotDescriptor } from '@/types';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { AuthPayload } from '@/types';
|
||||
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { useRouter, useRoute } from 'vue2-helpers/vue-router';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const defaultURL = window.location.origin || 'http://localhost:3000';
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({})
|
||||
export default class Login extends Vue {
|
||||
@Action setLoggedIn;
|
||||
export default defineComponent({
|
||||
name: 'Login',
|
||||
props: {
|
||||
inModal: { default: false, type: Boolean },
|
||||
},
|
||||
emits: ['loginResult'],
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const botStore = useBotStore();
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.nextBotId]: string;
|
||||
const nameState = ref<boolean | null>();
|
||||
const pwdState = ref<boolean | null>();
|
||||
const urlState = ref<boolean | null>();
|
||||
const errorMessage = ref<string>('');
|
||||
const errorMessageCORS = ref<boolean>(false);
|
||||
const formRef = ref<HTMLFormElement>();
|
||||
const auth = ref<AuthPayload>({
|
||||
botName: '',
|
||||
url: defaultURL,
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.selectedBot]: string;
|
||||
const emitLoginResult = (value: boolean) => {
|
||||
emit('loginResult', value);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action addBot!: (payload: BotDescriptor) => void;
|
||||
const checkFormValidity = () => {
|
||||
const valid = formRef.value?.checkValidity();
|
||||
nameState.value = valid || auth.value.username !== '';
|
||||
pwdState.value = valid || auth.value.password !== '';
|
||||
return valid;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action selectBot!: (botId: string) => void;
|
||||
const resetLogin = () => {
|
||||
auth.value.url = defaultURL;
|
||||
auth.value.username = '';
|
||||
auth.value.password = '';
|
||||
nameState.value = null;
|
||||
pwdState.value = null;
|
||||
errorMessage.value = '';
|
||||
};
|
||||
|
||||
@ftbot.Action allRefreshFull;
|
||||
const handleReset = (evt) => {
|
||||
evt.preventDefault();
|
||||
resetLogin();
|
||||
};
|
||||
|
||||
@Prop({ default: false }) inModal!: boolean;
|
||||
const handleSubmit = () => {
|
||||
// Exit when the form isn't valid
|
||||
if (!checkFormValidity()) {
|
||||
return;
|
||||
}
|
||||
errorMessage.value = '';
|
||||
const userService = useUserService(botStore.nextBotId);
|
||||
// Push the name to submitted names
|
||||
userService
|
||||
.login(auth.value)
|
||||
.then(() => {
|
||||
const botId = botStore.nextBotId;
|
||||
botStore.addBot({
|
||||
botName: auth.value.botName,
|
||||
botId,
|
||||
botUrl: auth.value.url,
|
||||
});
|
||||
if (botStore.selectedBot === '') {
|
||||
console.log(`selecting bot ${botId}`);
|
||||
botStore.selectBot(botId);
|
||||
}
|
||||
|
||||
$refs!: {
|
||||
form: HTMLFormElement;
|
||||
};
|
||||
|
||||
auth: AuthPayload = {
|
||||
botName: '',
|
||||
url: defaultURL,
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
@Emit('loginResult')
|
||||
emitLoginResult(value: boolean) {
|
||||
return value;
|
||||
}
|
||||
|
||||
nameState: boolean | null = null;
|
||||
|
||||
pwdState: boolean | null = null;
|
||||
|
||||
urlState: boolean | null = null;
|
||||
|
||||
errorMessage = '';
|
||||
|
||||
errorMessageCORS = false;
|
||||
|
||||
checkFormValidity() {
|
||||
const valid = this.$refs.form.checkValidity();
|
||||
this.nameState = valid || this.auth.username !== '';
|
||||
this.pwdState = valid || this.auth.password !== '';
|
||||
return valid;
|
||||
}
|
||||
|
||||
resetLogin() {
|
||||
this.auth.url = defaultURL;
|
||||
this.auth.username = '';
|
||||
this.auth.password = '';
|
||||
this.nameState = null;
|
||||
this.pwdState = null;
|
||||
this.errorMessage = '';
|
||||
}
|
||||
|
||||
handleReset(evt) {
|
||||
evt.preventDefault();
|
||||
this.resetLogin();
|
||||
}
|
||||
|
||||
handleOk(evt) {
|
||||
evt.preventDefault();
|
||||
this.handleSubmit();
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
// Exit when the form isn't valid
|
||||
if (!this.checkFormValidity()) {
|
||||
return;
|
||||
}
|
||||
this.errorMessage = '';
|
||||
const userService = useUserService(this.nextBotId);
|
||||
// Push the name to submitted names
|
||||
userService
|
||||
.login(this.auth)
|
||||
.then(() => {
|
||||
const botId = this.nextBotId;
|
||||
this.addBot({
|
||||
botName: this.auth.botName,
|
||||
botId,
|
||||
botUrl: this.auth.url,
|
||||
});
|
||||
if (this.selectedBot === '') {
|
||||
console.log(`selecting bot ${botId}`);
|
||||
this.selectBot(botId);
|
||||
}
|
||||
|
||||
this.emitLoginResult(true);
|
||||
this.allRefreshFull();
|
||||
if (this.inModal === false) {
|
||||
if (typeof this.$route.query.redirect === 'string') {
|
||||
const resolved = this.$router.resolve({ path: this.$route.query.redirect });
|
||||
if (resolved.route.name !== '404') {
|
||||
this.$router.push(resolved.route.path);
|
||||
emitLoginResult(true);
|
||||
botStore.allRefreshFull();
|
||||
if (props.inModal === false) {
|
||||
if (typeof route?.query.redirect === 'string') {
|
||||
const resolved = router.resolve({ path: route.query.redirect });
|
||||
if (resolved.route.name !== '404') {
|
||||
router.push(resolved.route.path);
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
} else {
|
||||
this.$router.push('/');
|
||||
router.push('/');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessageCORS.value = false;
|
||||
// this.nameState = false;
|
||||
console.error(error);
|
||||
if (error.response && error.response.status === 401) {
|
||||
nameState.value = false;
|
||||
pwdState.value = false;
|
||||
errorMessage.value =
|
||||
'Connected to bot, however Login failed, Username or Password wrong.';
|
||||
} else {
|
||||
this.$router.push('/');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.errorMessageCORS = false;
|
||||
// this.nameState = false;
|
||||
console.error(error.response);
|
||||
if (error.response && error.response.status === 401) {
|
||||
this.nameState = false;
|
||||
this.pwdState = false;
|
||||
this.errorMessage = 'Connected to bot, however Login failed, Username or Password wrong.';
|
||||
} else {
|
||||
this.urlState = false;
|
||||
this.errorMessage = `Login failed.
|
||||
urlState.value = false;
|
||||
errorMessage.value = `Login failed.
|
||||
Please verify that the bot is running, the Bot API is enabled and the URL is reachable.
|
||||
You can verify this by navigating to ${this.auth.url}/api/v1/ping to make sure the bot API is reachable`;
|
||||
if (this.auth.url !== window.location.origin) {
|
||||
this.errorMessageCORS = true;
|
||||
You can verify this by navigating to ${auth.value.url}/api/v1/ping to make sure the bot API is reachable`;
|
||||
if (auth.value.url !== window.location.origin) {
|
||||
errorMessageCORS.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error(this.errorMessage);
|
||||
this.emitLoginResult(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
console.error(errorMessage.value);
|
||||
emitLoginResult(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleOk = (evt) => {
|
||||
evt.preventDefault();
|
||||
handleSubmit();
|
||||
};
|
||||
|
||||
return {
|
||||
nameState,
|
||||
pwdState,
|
||||
urlState,
|
||||
errorMessage,
|
||||
auth,
|
||||
checkFormValidity,
|
||||
resetLogin,
|
||||
handleReset,
|
||||
handleOk,
|
||||
handleSubmit,
|
||||
formRef,
|
||||
errorMessageCORS,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<template>
|
||||
<v-chart v-if="currencies" :option="balanceChartOptions" :theme="getChartTheme" autoresize />
|
||||
<v-chart
|
||||
v-if="currencies"
|
||||
:option="balanceChartOptions"
|
||||
:theme="settingsStore.chartTheme"
|
||||
autoresize
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { Getter } from 'vuex-class';
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -21,6 +24,8 @@ import {
|
|||
|
||||
import { BalanceRecords } from '@/types';
|
||||
import { formatPriceCurrency } from '@/shared/formatters';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
use([
|
||||
PieChart,
|
||||
|
@ -32,65 +37,68 @@ use([
|
|||
LabelLayout,
|
||||
]);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'BalanceChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
})
|
||||
export default class BalanceChart extends Vue {
|
||||
@Prop({ required: true }) currencies!: BalanceRecords[];
|
||||
props: {
|
||||
currencies: { required: true, type: Array as () => BalanceRecords[] },
|
||||
showTitle: { required: false, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
@Prop({ default: false, type: Boolean }) showTitle!: boolean;
|
||||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
get balanceChartOptions(): EChartsOption {
|
||||
return {
|
||||
title: {
|
||||
text: 'Balance',
|
||||
show: this.showTitle,
|
||||
},
|
||||
center: ['50%', '50%'],
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['balance', 'currency', 'est_stake', 'free', 'used', 'stake'],
|
||||
source: this.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)})`;
|
||||
const balanceChartOptions = computed((): EChartsOption => {
|
||||
return {
|
||||
title: {
|
||||
text: 'Balance',
|
||||
show: props.showTitle,
|
||||
},
|
||||
},
|
||||
// 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,
|
||||
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'],
|
||||
},
|
||||
label: {
|
||||
formatter: '{b} - {d}%',
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return { balanceChartOptions, settingsStore };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -46,7 +46,7 @@
|
|||
<div class="ml-2">
|
||||
<b-select
|
||||
v-model="plotConfigName"
|
||||
:options="availablePlotConfigNames"
|
||||
:options="botStore.activeBot.availablePlotConfigNames"
|
||||
size="sm"
|
||||
@change="plotConfigChanged"
|
||||
>
|
||||
|
@ -67,8 +67,8 @@
|
|||
:trades="trades"
|
||||
:plot-config="plotConfig"
|
||||
:heikin-ashi="heikinAshi"
|
||||
:use-u-t-c="timezone === 'UTC'"
|
||||
:theme="getChartTheme"
|
||||
:use-u-t-c="settingsStore.timezone === 'UTC'"
|
||||
:theme="settingsStore.chartTheme"
|
||||
>
|
||||
</CandleChart>
|
||||
<div v-else class="m-auto">
|
||||
|
@ -89,184 +89,161 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
|
||||
import { Getter, namespace } from 'vuex-class';
|
||||
import {
|
||||
Trade,
|
||||
PairHistory,
|
||||
EMPTY_PLOTCONFIG,
|
||||
PlotConfig,
|
||||
PairCandlePayload,
|
||||
PairHistoryPayload,
|
||||
LoadingStatus,
|
||||
} from '@/types';
|
||||
import { Trade, PairHistory, EMPTY_PLOTCONFIG, PlotConfig, LoadingStatus } from '@/types';
|
||||
import CandleChart from '@/components/charts/CandleChart.vue';
|
||||
import PlotConfigurator from '@/components/charts/PlotConfigurator.vue';
|
||||
import { getCustomPlotConfig, getPlotConfigName } from '@/shared/storage';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { SettingsGetters } from '@/store/modules/settings';
|
||||
import vSelect from 'vue-select';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
||||
import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
@Component({ components: { CandleChart, PlotConfigurator, vSelect } })
|
||||
export default class CandleChartContainer extends Vue {
|
||||
@Prop({ required: true }) readonly availablePairs!: string[];
|
||||
export default defineComponent({
|
||||
name: 'CandleChartContainer',
|
||||
components: { CandleChart, PlotConfigurator, vSelect },
|
||||
props: {
|
||||
trades: { required: false, default: () => [], type: Array as () => Trade[] },
|
||||
availablePairs: { required: true, type: Array as () => string[] },
|
||||
timeframe: { required: true, type: String },
|
||||
historicView: { required: false, default: false, type: Boolean },
|
||||
plotConfigModal: { required: false, default: true, type: Boolean },
|
||||
/** Only required if historicView is true */
|
||||
timerange: { required: false, default: '', type: String },
|
||||
/** Only required if historicView is true */
|
||||
strategy: { required: false, default: '', type: String },
|
||||
},
|
||||
setup(props, { root }) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Prop({ required: true }) readonly timeframe!: string;
|
||||
const pair = ref('');
|
||||
const plotConfig = ref<PlotConfig>({ ...EMPTY_PLOTCONFIG });
|
||||
const plotConfigName = ref('');
|
||||
const heikinAshi = ref(false);
|
||||
const showPlotConfig = ref(props.plotConfigModal);
|
||||
|
||||
@Prop({ required: false, default: () => [] }) readonly trades!: Array<Trade>;
|
||||
|
||||
@Prop({ required: false, default: false }) historicView!: boolean;
|
||||
|
||||
@Prop({ required: false, default: true }) plotConfigModal!: boolean;
|
||||
|
||||
/** Only required if historicView is true */
|
||||
@Prop({ required: false, default: false }) timerange!: string;
|
||||
|
||||
/**
|
||||
* Only required if historicView is true
|
||||
*/
|
||||
@Prop({ required: false, default: false }) strategy!: string;
|
||||
|
||||
pair = '';
|
||||
|
||||
plotConfig: PlotConfig = { ...EMPTY_PLOTCONFIG };
|
||||
|
||||
plotConfigName = '';
|
||||
|
||||
showPlotConfig = this.plotConfigModal;
|
||||
|
||||
heikinAshi: boolean = false;
|
||||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.availablePlotConfigNames]!: string[];
|
||||
|
||||
@ftbot.Action setPlotConfigName;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.candleDataStatus]!: LoadingStatus;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.candleData]!: PairHistory;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.historyStatus]!: LoadingStatus;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.history]!: PairHistory;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.selectedPair]!: string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action public getPairCandles!: (payload: PairCandlePayload) => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action public getPairHistory!: (payload: PairHistoryPayload) => void;
|
||||
|
||||
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
||||
|
||||
get dataset(): PairHistory {
|
||||
if (this.historicView) {
|
||||
return this.history[`${this.pair}__${this.timeframe}`];
|
||||
}
|
||||
return this.candleData[`${this.pair}__${this.timeframe}`];
|
||||
}
|
||||
|
||||
get strategyName() {
|
||||
return this.strategy || this.dataset?.strategy || '';
|
||||
}
|
||||
|
||||
get datasetColumns() {
|
||||
return this.dataset ? this.dataset.columns : [];
|
||||
}
|
||||
|
||||
get isLoadingDataset(): boolean {
|
||||
if (this.historicView) {
|
||||
return this.historyStatus === 'loading';
|
||||
}
|
||||
|
||||
return this.candleDataStatus === 'loading';
|
||||
}
|
||||
|
||||
get noDatasetText(): string {
|
||||
const status = this.historicView ? this.historyStatus : this.candleDataStatus;
|
||||
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return 'Loading...';
|
||||
|
||||
case 'success':
|
||||
return 'No data available';
|
||||
|
||||
case 'error':
|
||||
return 'Failed to load data';
|
||||
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
get hasDataset(): boolean {
|
||||
return !!this.dataset;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
if (this.selectedPair) {
|
||||
this.pair = this.selectedPair;
|
||||
} else if (this.availablePairs.length > 0) {
|
||||
[this.pair] = this.availablePairs;
|
||||
}
|
||||
this.plotConfigName = getPlotConfigName();
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
|
||||
|
||||
if (!this.hasDataset) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
plotConfigChanged() {
|
||||
console.log('plotConfigChanged');
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
|
||||
this.setPlotConfigName(this.plotConfigName);
|
||||
}
|
||||
|
||||
showConfigurator() {
|
||||
if (this.plotConfigModal) {
|
||||
this.$bvModal.show('plotConfiguratorModal');
|
||||
} else {
|
||||
this.showPlotConfig = !this.showPlotConfig;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this.pair && this.timeframe) {
|
||||
if (this.historicView) {
|
||||
this.getPairHistory({
|
||||
pair: this.pair,
|
||||
timeframe: this.timeframe,
|
||||
timerange: this.timerange,
|
||||
strategy: this.strategy,
|
||||
});
|
||||
} else {
|
||||
this.getPairCandles({ pair: this.pair, timeframe: this.timeframe, limit: 500 });
|
||||
const dataset = computed((): PairHistory => {
|
||||
if (props.historicView) {
|
||||
return botStore.activeBot.history[`${pair.value}__${props.timeframe}`]?.data;
|
||||
}
|
||||
return botStore.activeBot.candleData[`${pair.value}__${props.timeframe}`]?.data;
|
||||
});
|
||||
const strategyName = computed(() => props.strategy || dataset.value?.strategy || '');
|
||||
const datasetColumns = computed(() => (dataset.value ? dataset.value.columns : []));
|
||||
const hasDataset = computed(() => !!dataset.value);
|
||||
const isLoadingDataset = computed((): boolean => {
|
||||
if (props.historicView) {
|
||||
return botStore.activeBot.historyStatus === LoadingStatus.loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('availablePairs')
|
||||
watchAvailablePairs() {
|
||||
if (!this.availablePairs.find((pair) => pair === this.pair)) {
|
||||
[this.pair] = this.availablePairs;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
return botStore.activeBot.candleDataStatus === LoadingStatus.loading;
|
||||
});
|
||||
const noDatasetText = computed((): string => {
|
||||
const status = props.historicView
|
||||
? botStore.activeBot.historyStatus
|
||||
: botStore.activeBot.candleDataStatus;
|
||||
|
||||
@Watch(BotStoreGetters.selectedPair)
|
||||
watchSelectedPair() {
|
||||
this.pair = this.selectedPair;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
case LoadingStatus.loading:
|
||||
return 'Loading...';
|
||||
|
||||
case LoadingStatus.success:
|
||||
return 'No data available';
|
||||
|
||||
case LoadingStatus.error:
|
||||
return 'Failed to load data';
|
||||
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
});
|
||||
|
||||
const plotConfigChanged = () => {
|
||||
console.log('plotConfigChanged');
|
||||
plotConfig.value = getCustomPlotConfig(plotConfigName.value);
|
||||
botStore.activeBot.setPlotConfigName(plotConfigName.value);
|
||||
};
|
||||
|
||||
const showConfigurator = () => {
|
||||
if (props.plotConfigModal) {
|
||||
root.$bvModal.show('plotConfiguratorModal');
|
||||
} else {
|
||||
showPlotConfig.value = !showPlotConfig.value;
|
||||
}
|
||||
};
|
||||
const refresh = () => {
|
||||
if (pair.value && props.timeframe) {
|
||||
if (props.historicView) {
|
||||
botStore.activeBot.getPairHistory({
|
||||
pair: pair.value,
|
||||
timeframe: props.timeframe,
|
||||
timerange: props.timerange,
|
||||
strategy: props.strategy,
|
||||
});
|
||||
} else {
|
||||
botStore.activeBot.getPairCandles({
|
||||
pair: pair.value,
|
||||
timeframe: props.timeframe,
|
||||
limit: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.availablePairs,
|
||||
() => {
|
||||
if (!props.availablePairs.find((p) => p === pair.value)) {
|
||||
[pair.value] = props.availablePairs;
|
||||
refresh();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => botStore.activeBot.selectedPair,
|
||||
() => {
|
||||
pair.value = botStore.activeBot.selectedPair;
|
||||
refresh();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (botStore.activeBot.selectedPair) {
|
||||
pair.value = botStore.activeBot.selectedPair;
|
||||
} else if (props.availablePairs.length > 0) {
|
||||
[pair.value] = props.availablePairs;
|
||||
}
|
||||
plotConfigName.value = getPlotConfigName();
|
||||
plotConfig.value = getCustomPlotConfig(plotConfigName.value);
|
||||
|
||||
if (!hasDataset) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
settingsStore,
|
||||
history,
|
||||
dataset,
|
||||
strategyName,
|
||||
datasetColumns,
|
||||
isLoadingDataset,
|
||||
noDatasetText,
|
||||
hasDataset,
|
||||
heikinAshi,
|
||||
plotConfigChanged,
|
||||
showPlotConfig,
|
||||
showConfigurator,
|
||||
refresh,
|
||||
plotConfigName,
|
||||
pair,
|
||||
plotConfig,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<template>
|
||||
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="getChartTheme" />
|
||||
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { Getter } from 'vuex-class';
|
||||
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -21,6 +18,8 @@ import {
|
|||
} from 'echarts/components';
|
||||
|
||||
import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types';
|
||||
import { defineComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
use([
|
||||
BarChart,
|
||||
|
@ -38,162 +37,166 @@ use([
|
|||
// Define Column labels here to avoid typos
|
||||
const CHART_PROFIT = 'Profit';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'CumProfitChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
})
|
||||
export default class CumProfitChart extends Vue {
|
||||
@Prop({ required: true }) trades!: ClosedTrade[];
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => ClosedTrade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
profitColumn: { default: 'close_profit_abs', type: String },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const botList = ref<string[]>([]);
|
||||
const cumulativeData = ref<{ date: number; profit: any }[]>([]);
|
||||
|
||||
@Prop({ default: true, type: Boolean }) showTitle!: boolean;
|
||||
watch(
|
||||
() => props.trades,
|
||||
() => {
|
||||
botList.value = [];
|
||||
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;
|
||||
|
||||
@Prop({ default: 'close_profit_abs' }) profitColumn!: string;
|
||||
for (let i = 0, len = closedTrades.length; i < len; i += 1) {
|
||||
const trade = closedTrades[i];
|
||||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
botList: string[] = [];
|
||||
|
||||
get cumulativeData() {
|
||||
this.botList = [];
|
||||
const res: CumProfitData[] = [];
|
||||
const resD: CumProfitDataPerDate = {};
|
||||
const closedTrades = this.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[this.profitColumn]) {
|
||||
profit += trade[this.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[this.profitColumn];
|
||||
if (resD[trade.close_timestamp][trade.botId]) {
|
||||
resD[trade.close_timestamp][trade.botId] += trade[this.profitColumn];
|
||||
} else {
|
||||
resD[trade.close_timestamp][trade.botId] = profit;
|
||||
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(trade.close_date, profit);
|
||||
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
|
||||
if (!botList.value.includes(trade.botId)) {
|
||||
botList.value.push(trade.botId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log(trade.close_date, profit);
|
||||
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
|
||||
if (!this.botList.includes(trade.botId)) {
|
||||
this.botList.push(trade.botId);
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log(resD);
|
||||
// console.log(resD);
|
||||
|
||||
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];
|
||||
cumulativeData.value = 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,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
// xAxisIndex: [0],
|
||||
type: 'slider',
|
||||
bottom: 10,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: CHART_PROFIT,
|
||||
animation: true,
|
||||
step: 'end',
|
||||
lineStyle: {
|
||||
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
},
|
||||
itemStyle: {
|
||||
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
},
|
||||
// symbol: 'none',
|
||||
},
|
||||
],
|
||||
};
|
||||
// TODO: maybe have profit lines per bot?
|
||||
// this.botList.forEach((botId: string) => {
|
||||
// console.log('bot', botId);
|
||||
// chartOptionsLoc.series.push({
|
||||
// type: 'line',
|
||||
// name: botId,
|
||||
// animation: true,
|
||||
// step: 'end',
|
||||
// lineStyle: {
|
||||
// color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
// },
|
||||
// itemStylesettingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
// },
|
||||
// // symbol: 'none',
|
||||
// });
|
||||
// });
|
||||
return obj;
|
||||
return chartOptionsLoc;
|
||||
});
|
||||
}
|
||||
|
||||
get chartOptions(): EChartsOption {
|
||||
const chartOptionsLoc: EChartsOption = {
|
||||
title: {
|
||||
text: 'Cumulative Profit',
|
||||
show: this.showTitle,
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['date', 'profit'],
|
||||
source: this.cumulativeData,
|
||||
},
|
||||
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,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
// xAxisIndex: [0],
|
||||
type: 'slider',
|
||||
bottom: 10,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: CHART_PROFIT,
|
||||
animation: true,
|
||||
step: 'end',
|
||||
lineStyle: {
|
||||
color: this.getChartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
},
|
||||
itemStyle: {
|
||||
color: this.getChartTheme === '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: this.getChartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
// },
|
||||
// itemStyle: {
|
||||
// color: this.getChartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
// },
|
||||
// // symbol: 'none',
|
||||
// });
|
||||
// });
|
||||
return chartOptionsLoc;
|
||||
}
|
||||
}
|
||||
return { settingsStore, cumulativeData, chartOptions };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<template>
|
||||
<v-chart v-if="dailyStats.data" :option="dailyChartOptions" :theme="getChartTheme" autoresize />
|
||||
<v-chart
|
||||
v-if="dailyStats.data"
|
||||
:option="dailyChartOptions"
|
||||
:theme="settingsStore.chartTheme"
|
||||
autoresize
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useGetters } from 'vuex-composition-helpers';
|
||||
import { ref, defineComponent, computed, ComputedRef } from '@vue/composition-api';
|
||||
import { defineComponent, computed, ComputedRef } from '@vue/composition-api';
|
||||
import ECharts from 'vue-echarts';
|
||||
// import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -21,6 +25,8 @@ import {
|
|||
} from 'echarts/components';
|
||||
|
||||
import { DailyReturnValue } from '@/types';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
use([
|
||||
BarChart,
|
||||
|
@ -54,108 +60,107 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup(props) {
|
||||
const { getChartTheme } = useGetters(['getChartTheme']);
|
||||
const absoluteMin: ComputedRef<number> = computed(() =>
|
||||
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: ComputedRef<number> = computed(() =>
|
||||
const absoluteMax = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
// : Ref<EChartsOption>
|
||||
const dailyChartOptions = ref({
|
||||
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',
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
inverse: true,
|
||||
legend: {
|
||||
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
],
|
||||
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: {
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
},
|
||||
],
|
||||
visualMap: [
|
||||
{
|
||||
dimension: 1,
|
||||
seriesIndex: 0,
|
||||
show: false,
|
||||
pieces: [
|
||||
{
|
||||
max: 0.0,
|
||||
min: absoluteMin.value,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
max: absoluteMax.value,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
},
|
||||
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)',
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_ABS_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 40,
|
||||
},
|
||||
yAxisIndex: 1,
|
||||
},
|
||||
],
|
||||
{
|
||||
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,
|
||||
getChartTheme,
|
||||
settingsStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,15 +3,14 @@
|
|||
v-if="trades.length > 0"
|
||||
:option="hourlyChartOptions"
|
||||
autoresize
|
||||
:theme="getChartTheme"
|
||||
:theme="settingsStore.chartTheme"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { Getter } from 'vuex-class';
|
||||
|
||||
import ECharts from 'vue-echarts';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
|
||||
import { Trade } from '@/types';
|
||||
import { timestampHour } from '@/shared/formatters';
|
||||
|
@ -46,122 +45,122 @@ use([
|
|||
const CHART_PROFIT = 'Profit %';
|
||||
const CHART_TRADE_COUNT = 'Trade Count';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'HourlyChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
})
|
||||
export default class HourlyChart extends Vue {
|
||||
// TODO: This chart is not used at the moment!
|
||||
@Prop({ required: true }) trades!: Trade[];
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => Trade[] },
|
||||
showTitle: { default: true, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
@Prop({ default: true, type: Boolean }) showTitle!: boolean;
|
||||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
get hourlyData() {
|
||||
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 = this.trades.length; i < len; i += 1) {
|
||||
const trade = this.trades[i];
|
||||
if (trade.close_timestamp) {
|
||||
const hour = timestampHour(trade.close_timestamp);
|
||||
|
||||
res[hour].profit += trade.profit_ratio;
|
||||
res[hour].count += 1;
|
||||
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 };
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
get hourlyChartOptions(): EChartsOption {
|
||||
return {
|
||||
title: {
|
||||
text: 'Hourly Profit',
|
||||
show: this.showTitle,
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['hourDesc', 'profit', 'count'],
|
||||
source: this.hourlyData,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
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,
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['hourDesc', 'profit', 'count'],
|
||||
source: hourlyData.value,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [CHART_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
legend: {
|
||||
data: [CHART_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
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: [
|
||||
{
|
||||
max: 0.0,
|
||||
min: -2,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
max: 2,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
},
|
||||
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: [
|
||||
{
|
||||
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)',
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: CHART_PROFIT,
|
||||
animation: false,
|
||||
// symbol: 'none',
|
||||
},
|
||||
yAxisIndex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
{
|
||||
type: 'bar',
|
||||
name: CHART_TRADE_COUNT,
|
||||
animation: false,
|
||||
itemStyle: {
|
||||
color: 'rgba(150,150,150,0.3)',
|
||||
},
|
||||
yAxisIndex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
return { settingsStore, hourlyChartOptions };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -120,238 +120,225 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { PlotConfig, EMPTY_PLOTCONFIG, IndicatorConfig } from '@/types';
|
||||
import { getCustomPlotConfig } from '@/shared/storage';
|
||||
import PlotIndicator from '@/components/charts/PlotIndicator.vue';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { AlertActions } from '@/store/modules/alerts';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { showAlert } from '@/stores/alerts';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent, computed, ref, watch, onMounted } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const alerts = namespace(StoreModules.alerts);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'PlotConfigurator',
|
||||
components: { PlotIndicator },
|
||||
})
|
||||
export default class PlotConfigurator extends Vue {
|
||||
@Prop({ required: true }) value!: PlotConfig;
|
||||
props: {
|
||||
value: { type: Object as () => PlotConfig, required: true },
|
||||
columns: { required: true, type: Array as () => string[] },
|
||||
asModal: { required: false, default: true, type: Boolean },
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Prop({ required: true }) columns!: Array<string>;
|
||||
const plotConfig = ref<PlotConfig>(EMPTY_PLOTCONFIG);
|
||||
|
||||
@Prop({ required: false, default: true }) asModal!: boolean;
|
||||
const plotConfigNameLoc = ref('default');
|
||||
const newSubplotName = ref('');
|
||||
const selIndicatorName = ref('');
|
||||
const addNewIndicator = ref(false);
|
||||
const showConfig = ref(false);
|
||||
const selSubPlot = ref('main_plot');
|
||||
const tempPlotConfig = ref<PlotConfig>();
|
||||
const tempPlotConfigValid = ref(true);
|
||||
|
||||
@Emit('input')
|
||||
emitPlotConfig() {
|
||||
return this.plotConfig;
|
||||
}
|
||||
const isMainPlot = computed(() => {
|
||||
return selSubPlot.value === 'main_plot';
|
||||
});
|
||||
|
||||
@ftbot.Action getStrategyPlotConfig;
|
||||
const currentPlotConfig = computed(() => {
|
||||
if (isMainPlot.value) {
|
||||
return plotConfig.value.main_plot;
|
||||
}
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.strategyPlotConfig];
|
||||
return plotConfig.value.subplots[selSubPlot.value];
|
||||
});
|
||||
const subplots = computed((): string[] => {
|
||||
// Subplot keys (for selection window)
|
||||
return ['main_plot', ...Object.keys(plotConfig.value.subplots)];
|
||||
});
|
||||
const usedColumns = computed((): string[] => {
|
||||
if (isMainPlot.value) {
|
||||
return Object.keys(plotConfig.value.main_plot);
|
||||
}
|
||||
if (selSubPlot.value in plotConfig.value.subplots) {
|
||||
return Object.keys(plotConfig.value.subplots[selSubPlot.value]);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
get selIndicator(): Record<string, IndicatorConfig> {
|
||||
if (this.addNewIndicator) {
|
||||
return {};
|
||||
}
|
||||
if (this.selIndicatorName) {
|
||||
return {
|
||||
[this.selIndicatorName]: this.currentPlotConfig[this.selIndicatorName],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const addIndicator = (newIndicator: Record<string, IndicatorConfig>) => {
|
||||
console.log(plotConfig.value);
|
||||
|
||||
set selIndicator(newValue: Record<string, IndicatorConfig>) {
|
||||
// console.log('newValue', newValue);
|
||||
const name = Object.keys(newValue)[0];
|
||||
// this.currentPlotConfig[this.selIndicatorName] = { ...newValue[name] };
|
||||
// this.emitPlotConfig();
|
||||
if (name && newValue) {
|
||||
this.addIndicator(newValue);
|
||||
} else {
|
||||
this.addNewIndicator = false;
|
||||
}
|
||||
}
|
||||
// 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 };
|
||||
} else {
|
||||
console.log(`Adding ${name} to ${selSubPlot.value}`);
|
||||
plotConfig.value.subplots[selSubPlot.value][name] = { ...indicator };
|
||||
}
|
||||
|
||||
plotConfig: PlotConfig = EMPTY_PLOTCONFIG;
|
||||
|
||||
plotOptions = [
|
||||
{ text: 'Main Plot', value: 'main_plot' },
|
||||
{ text: 'Subplots', value: 'subplots' },
|
||||
];
|
||||
|
||||
plotConfigNameLoc = 'default';
|
||||
|
||||
newSubplotName = '';
|
||||
|
||||
selAvailableIndicator = '';
|
||||
|
||||
selIndicatorName = '';
|
||||
|
||||
addNewIndicator = false;
|
||||
|
||||
showConfig = false;
|
||||
|
||||
selSubPlot = 'main_plot';
|
||||
|
||||
tempPlotConfig?: PlotConfig = undefined;
|
||||
|
||||
tempPlotConfigValid = true;
|
||||
|
||||
@ftbot.Action saveCustomPlotConfig;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action updatePlotConfigName!: (plotConfigName: string) => void;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.plotConfigName]!: string;
|
||||
|
||||
@alerts.Action [AlertActions.addAlert];
|
||||
|
||||
get plotConfigJson() {
|
||||
return JSON.stringify(this.plotConfig, null, 2);
|
||||
}
|
||||
|
||||
set plotConfigJson(newValue: string) {
|
||||
try {
|
||||
this.tempPlotConfig = JSON.parse(newValue);
|
||||
// TODO: Should Validate schema validity (should be PlotConfig type...)
|
||||
this.tempPlotConfigValid = true;
|
||||
} catch (err) {
|
||||
this.tempPlotConfigValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
get subplots(): string[] {
|
||||
// Subplot keys (for selection window)
|
||||
return ['main_plot', ...Object.keys(this.plotConfig.subplots)];
|
||||
}
|
||||
|
||||
get usedColumns(): string[] {
|
||||
if (this.isMainPlot) {
|
||||
return Object.keys(this.plotConfig.main_plot);
|
||||
}
|
||||
if (this.selSubPlot in this.plotConfig.subplots) {
|
||||
return Object.keys(this.plotConfig.subplots[this.selSubPlot]);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
get isMainPlot() {
|
||||
return this.selSubPlot === 'main_plot';
|
||||
}
|
||||
|
||||
get currentPlotConfig() {
|
||||
if (this.isMainPlot) {
|
||||
return this.plotConfig.main_plot;
|
||||
}
|
||||
|
||||
return this.plotConfig.subplots[this.selSubPlot];
|
||||
}
|
||||
|
||||
mounted() {
|
||||
console.log('Config Mounted', this.value);
|
||||
this.plotConfig = this.value;
|
||||
this.plotConfigNameLoc = this.plotConfigName;
|
||||
}
|
||||
|
||||
@Watch('value')
|
||||
watchValue() {
|
||||
this.plotConfig = this.value;
|
||||
this.plotConfigNameLoc = this.plotConfigName;
|
||||
}
|
||||
|
||||
addIndicator(newIndicator: Record<string, IndicatorConfig>) {
|
||||
console.log(this.plotConfig);
|
||||
|
||||
const { plotConfig } = this;
|
||||
const name = Object.keys(newIndicator)[0];
|
||||
const indicator = newIndicator[name];
|
||||
if (this.isMainPlot) {
|
||||
console.log(`Adding ${name} to MainPlot`);
|
||||
plotConfig.main_plot[name] = { ...indicator };
|
||||
} else {
|
||||
console.log(`Adding ${name} to ${this.selSubPlot}`);
|
||||
plotConfig.subplots[this.selSubPlot][name] = { ...indicator };
|
||||
}
|
||||
|
||||
this.plotConfig = { ...plotConfig };
|
||||
// Reset random color
|
||||
this.addNewIndicator = false;
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
|
||||
removeIndicator() {
|
||||
console.log(this.plotConfig);
|
||||
const { plotConfig } = this;
|
||||
if (this.isMainPlot) {
|
||||
console.log(`Removing ${this.selIndicatorName} from MainPlot`);
|
||||
delete plotConfig.main_plot[this.selIndicatorName];
|
||||
} else {
|
||||
console.log(`Removing ${this.selIndicatorName} from ${this.selSubPlot}`);
|
||||
delete plotConfig.subplots[this.selSubPlot][this.selIndicatorName];
|
||||
}
|
||||
|
||||
this.plotConfig = { ...plotConfig };
|
||||
console.log(this.plotConfig);
|
||||
this.selIndicatorName = '';
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
|
||||
addSubplot() {
|
||||
this.plotConfig.subplots = {
|
||||
...this.plotConfig.subplots,
|
||||
[this.newSubplotName]: {},
|
||||
plotConfig.value = { ...plotConfig.value };
|
||||
// Reset random color
|
||||
addNewIndicator.value = false;
|
||||
emit('input', plotConfig.value);
|
||||
};
|
||||
this.selSubPlot = this.newSubplotName;
|
||||
this.newSubplotName = '';
|
||||
|
||||
console.log(this.plotConfig);
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
const selIndicator = computed({
|
||||
get() {
|
||||
if (addNewIndicator.value) {
|
||||
return {};
|
||||
}
|
||||
if (selIndicatorName.value) {
|
||||
return {
|
||||
[selIndicatorName.value]: currentPlotConfig.value[selIndicatorName.value],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
set(newValue: Record<string, IndicatorConfig>) {
|
||||
// console.log('newValue', newValue);
|
||||
const name = Object.keys(newValue)[0];
|
||||
// this.currentPlotConfig[this.selIndicatorName] = { ...newValue[name] };
|
||||
// this.emitPlotConfig();
|
||||
if (name && newValue) {
|
||||
addIndicator(newValue);
|
||||
} else {
|
||||
addNewIndicator.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
delSubplot() {
|
||||
delete this.plotConfig.subplots[this.selSubPlot];
|
||||
this.plotConfig.subplots = { ...this.plotConfig.subplots };
|
||||
this.selSubPlot = '';
|
||||
}
|
||||
const plotConfigJson = computed({
|
||||
get() {
|
||||
return JSON.stringify(plotConfig.value, null, 2);
|
||||
},
|
||||
set(newValue: string) {
|
||||
try {
|
||||
tempPlotConfig.value = JSON.parse(newValue);
|
||||
// TODO: Should Validate schema validity (should be PlotConfig type...)
|
||||
tempPlotConfigValid.value = true;
|
||||
} catch (err) {
|
||||
tempPlotConfigValid.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
savePlotConfig() {
|
||||
this.saveCustomPlotConfig({ [this.plotConfigNameLoc]: this.plotConfig });
|
||||
}
|
||||
const 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];
|
||||
} else {
|
||||
console.log(`Removing ${selIndicatorName.value} from ${selSubPlot.value}`);
|
||||
delete plotConfig.value.subplots[selSubPlot.value][selIndicatorName.value];
|
||||
}
|
||||
|
||||
loadPlotConfig() {
|
||||
this.plotConfig = getCustomPlotConfig(this.plotConfigNameLoc);
|
||||
console.log(this.plotConfig);
|
||||
console.log('loading config');
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
plotConfig.value = { ...plotConfig.value };
|
||||
console.log(plotConfig.value);
|
||||
selIndicatorName.value = '';
|
||||
emit('input', plotConfig.value);
|
||||
};
|
||||
const addSubplot = () => {
|
||||
plotConfig.value.subplots = {
|
||||
...plotConfig.value.subplots,
|
||||
[newSubplotName.value]: {},
|
||||
};
|
||||
selSubPlot.value = newSubplotName.value;
|
||||
newSubplotName.value = '';
|
||||
|
||||
loadConfigFromString() {
|
||||
// this.plotConfig = JSON.parse();
|
||||
if (this.tempPlotConfig !== undefined && this.tempPlotConfigValid) {
|
||||
this.plotConfig = this.tempPlotConfig;
|
||||
this.emitPlotConfig();
|
||||
}
|
||||
}
|
||||
console.log(plotConfig.value);
|
||||
emit('input', plotConfig.value);
|
||||
};
|
||||
|
||||
resetConfig() {
|
||||
this.plotConfig = { ...EMPTY_PLOTCONFIG };
|
||||
}
|
||||
const delSubplot = () => {
|
||||
delete plotConfig.value.subplots[selSubPlot.value];
|
||||
plotConfig.value.subplots = { ...plotConfig.value.subplots };
|
||||
selSubPlot.value = '';
|
||||
};
|
||||
const loadPlotConfig = () => {
|
||||
plotConfig.value = getCustomPlotConfig(plotConfigNameLoc.value);
|
||||
console.log(plotConfig.value);
|
||||
console.log('loading config');
|
||||
emit('input', plotConfig.value);
|
||||
};
|
||||
|
||||
async loadPlotConfigFromStrategy() {
|
||||
try {
|
||||
await this.getStrategyPlotConfig();
|
||||
this.plotConfig = this.strategyPlotConfig;
|
||||
this.emitPlotConfig();
|
||||
} catch (data) {
|
||||
//
|
||||
this.addAlert({ message: 'Failed to load Plot configuration from Strategy.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
const loadConfigFromString = () => {
|
||||
// this.plotConfig = JSON.parse();
|
||||
if (tempPlotConfig.value !== undefined && tempPlotConfigValid.value) {
|
||||
plotConfig.value = tempPlotConfig.value;
|
||||
emit('input', plotConfig.value);
|
||||
}
|
||||
};
|
||||
const resetConfig = () => {
|
||||
plotConfig.value = { ...EMPTY_PLOTCONFIG };
|
||||
};
|
||||
const loadPlotConfigFromStrategy = async () => {
|
||||
try {
|
||||
await botStore.activeBot.getStrategyPlotConfig();
|
||||
if (botStore.activeBot.strategyPlotConfig) {
|
||||
plotConfig.value = botStore.activeBot.strategyPlotConfig;
|
||||
emit('input', plotConfig.value);
|
||||
}
|
||||
} catch (data) {
|
||||
//
|
||||
showAlert('Failed to load Plot configuration from Strategy.');
|
||||
}
|
||||
};
|
||||
|
||||
const savePlotConfig = () => {
|
||||
botStore.activeBot.saveCustomPlotConfig({ [plotConfigNameLoc.value]: plotConfig.value });
|
||||
};
|
||||
|
||||
watch(props.value, () => {
|
||||
console.log('config value');
|
||||
plotConfig.value = props.value;
|
||||
plotConfigNameLoc.value = botStore.activeBot.plotConfigName;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Config Mounted', props.value);
|
||||
plotConfig.value = props.value;
|
||||
plotConfigNameLoc.value = botStore.activeBot.plotConfigName;
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
removeIndicator,
|
||||
addSubplot,
|
||||
delSubplot,
|
||||
loadPlotConfig,
|
||||
loadConfigFromString,
|
||||
resetConfig,
|
||||
loadPlotConfigFromStrategy,
|
||||
savePlotConfig,
|
||||
showConfig,
|
||||
addNewIndicator,
|
||||
selIndicatorName,
|
||||
usedColumns,
|
||||
selSubPlot,
|
||||
newSubplotName,
|
||||
subplots,
|
||||
plotConfigNameLoc,
|
||||
selIndicator,
|
||||
plotConfigJson,
|
||||
tempPlotConfigValid,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -362,6 +349,7 @@ export default class PlotConfigurator extends Vue {
|
|||
.form-group {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
|
|
|
@ -71,70 +71,80 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { ChartType, IndicatorConfig } from '@/types';
|
||||
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import randomColor from '@/shared/randomColor';
|
||||
|
||||
@Component({})
|
||||
export default class PlotIndicator extends Vue {
|
||||
@Prop({ required: true }) value!: Record<string, IndicatorConfig>;
|
||||
import { defineComponent, computed, ref, watch } from '@vue/composition-api';
|
||||
|
||||
@Prop({ required: true }) columns!: string[];
|
||||
export default defineComponent({
|
||||
name: 'PlotIndicator',
|
||||
props: {
|
||||
value: { required: true, type: Object as () => Record<string, IndicatorConfig> },
|
||||
columns: { required: true, type: Array as () => string[] },
|
||||
addNew: { required: true, type: Boolean },
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const selColor = ref(randomColor());
|
||||
const graphType = ref<ChartType>(ChartType.line);
|
||||
const availableGraphTypes = ref(Object.keys(ChartType));
|
||||
const selAvailableIndicator = ref('');
|
||||
const cancelled = ref(false);
|
||||
|
||||
@Prop({ required: true }) addNew!: boolean;
|
||||
|
||||
@Emit('input')
|
||||
emitIndicator() {
|
||||
return this.combinedIndicator;
|
||||
}
|
||||
|
||||
selColor = randomColor();
|
||||
|
||||
graphType: ChartType = ChartType.line;
|
||||
|
||||
availableGraphTypes = Object.keys(ChartType);
|
||||
|
||||
selAvailableIndicator = '';
|
||||
|
||||
cancelled = false;
|
||||
|
||||
@Watch('value')
|
||||
watchValue() {
|
||||
[this.selAvailableIndicator] = Object.keys(this.value);
|
||||
this.cancelled = false;
|
||||
if (this.selAvailableIndicator && this.value) {
|
||||
this.selColor = this.value[this.selAvailableIndicator].color || randomColor();
|
||||
this.graphType = this.value[this.selAvailableIndicator].type || ChartType.line;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('selColor')
|
||||
watchColor() {
|
||||
if (!this.addNew) {
|
||||
this.emitIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
clickCancel() {
|
||||
this.cancelled = true;
|
||||
this.emitIndicator();
|
||||
}
|
||||
|
||||
get combinedIndicator() {
|
||||
if (this.cancelled || !this.selAvailableIndicator) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
[this.selAvailableIndicator]: {
|
||||
color: this.selColor,
|
||||
type: this.graphType,
|
||||
},
|
||||
const newColor = () => {
|
||||
selColor.value = randomColor();
|
||||
};
|
||||
}
|
||||
|
||||
newColor() {
|
||||
this.selColor = randomColor();
|
||||
}
|
||||
}
|
||||
const combinedIndicator = computed(() => {
|
||||
if (cancelled.value || !selAvailableIndicator.value) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
[selAvailableIndicator.value]: {
|
||||
color: selColor.value,
|
||||
type: graphType.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
const emitIndicator = () => {
|
||||
emit('input', combinedIndicator.value);
|
||||
};
|
||||
|
||||
const clickCancel = () => {
|
||||
cancelled.value = true;
|
||||
emitIndicator();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
[selAvailableIndicator.value] = Object.keys(props.value);
|
||||
cancelled.value = false;
|
||||
if (selAvailableIndicator.value && props.value) {
|
||||
selColor.value = props.value[selAvailableIndicator.value].color || randomColor();
|
||||
graphType.value = props.value[selAvailableIndicator.value].type || ChartType.line;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(selColor, () => {
|
||||
if (!props.addNew) {
|
||||
emitIndicator();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
selColor,
|
||||
graphType,
|
||||
availableGraphTypes,
|
||||
selAvailableIndicator,
|
||||
cancelled,
|
||||
combinedIndicator,
|
||||
newColor,
|
||||
emitIndicator,
|
||||
clickCancel,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<v-chart v-if="trades.length > 0" :option="chartOptions" autoresize :theme="getChartTheme" />
|
||||
<v-chart
|
||||
v-if="trades.length > 0"
|
||||
:option="chartOptions"
|
||||
autoresize
|
||||
:theme="settingsStore.chartTheme"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { Getter } from 'vuex-class';
|
||||
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -23,7 +25,8 @@ import {
|
|||
} from 'echarts/components';
|
||||
|
||||
import { ClosedTrade } from '@/types';
|
||||
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { timestampms } from '@/shared/formatters';
|
||||
|
||||
use([
|
||||
|
@ -45,142 +48,145 @@ use([
|
|||
const CHART_PROFIT = 'Profit %';
|
||||
const CHART_COLOR = '#9be0a8';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'TradesLogChart',
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
})
|
||||
export default class TradesLogChart extends Vue {
|
||||
@Prop({ required: true }) trades!: ClosedTrade[];
|
||||
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;
|
||||
});
|
||||
|
||||
@Prop({ default: true, type: Boolean }) showTitle!: boolean;
|
||||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
get chartData() {
|
||||
const res: (number | string)[][] = [];
|
||||
const sortedTrades = this.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;
|
||||
}
|
||||
|
||||
get chartOptions(): EChartsOption {
|
||||
const { chartData } = this;
|
||||
// Show a maximum of 50 trades by default - allowing to zoom out further.
|
||||
const datazoomStart = chartData.length > 0 ? (1 - 50 / chartData.length) * 100 : 100;
|
||||
return {
|
||||
title: {
|
||||
text: 'Trades log',
|
||||
show: this.showTitle,
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['date', 'profit'],
|
||||
source: chartData,
|
||||
},
|
||||
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]} %`;
|
||||
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,
|
||||
},
|
||||
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',
|
||||
show: false,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
show: false,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: CHART_PROFIT,
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 30,
|
||||
],
|
||||
grid: {
|
||||
bottom: 80,
|
||||
},
|
||||
],
|
||||
grid: {
|
||||
bottom: 80,
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: datazoomStart,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
type: 'slider',
|
||||
bottom: 10,
|
||||
start: datazoomStart,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
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: {
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: datazoomStart,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
position: 'top',
|
||||
rotate: 90,
|
||||
offset: [7.5, 7.5],
|
||||
formatter: '{@[1]} %',
|
||||
color: this.getChartTheme === 'dark' ? '#c2c2c2' : '#3c3c3c',
|
||||
type: 'slider',
|
||||
bottom: 10,
|
||||
start: datazoomStart,
|
||||
end: 100,
|
||||
},
|
||||
encode: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
],
|
||||
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,
|
||||
itemStyle: {
|
||||
color: CHART_COLOR,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return { settingsStore, chartData, chartOptions };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class="btn btn-secondary float-right"
|
||||
title="Refresh"
|
||||
aria-label="Refresh"
|
||||
@click="getBacktestHistory"
|
||||
@click="botStore.activeBot.getBacktestHistory"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
|
@ -12,13 +12,13 @@
|
|||
Load Historic results from disk. You can click on multiple results to load all of them into
|
||||
freqUI.
|
||||
</p>
|
||||
<b-list-group v-if="backtestHistoryList" class="ml-2">
|
||||
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ml-2">
|
||||
<b-list-group-item
|
||||
v-for="(res, idx) in backtestHistoryList"
|
||||
v-for="(res, idx) in botStore.activeBot.backtestHistoryList"
|
||||
:key="idx"
|
||||
class="d-flex justify-content-between align-items-center py-1 mb-1"
|
||||
button
|
||||
@click="getBacktestHistoryResult(res)"
|
||||
@click="botStore.activeBot.getBacktestHistoryResult(res)"
|
||||
>
|
||||
<strong>{{ res.strategy }}</strong>
|
||||
backtested on: {{ timestampms(res.backtest_start_time * 1000) }}
|
||||
|
@ -30,29 +30,20 @@
|
|||
|
||||
<script>
|
||||
import { defineComponent, onMounted } from '@vue/composition-api';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { useNamespacedActions, useNamespacedGetters } from 'vuex-composition-helpers';
|
||||
import { timestampms } from '@/shared/formatters';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { getBacktestHistory, getBacktestHistoryResult } = useNamespacedActions(
|
||||
StoreModules.ftbot,
|
||||
['getBacktestHistory', 'getBacktestHistoryResult'],
|
||||
);
|
||||
const { backtestHistoryList } = useNamespacedGetters(StoreModules.ftbot, [
|
||||
'backtestHistoryList',
|
||||
]);
|
||||
const botStore = useBotStore();
|
||||
|
||||
onMounted(() => {
|
||||
getBacktestHistory();
|
||||
botStore.activeBot.getBacktestHistory();
|
||||
});
|
||||
|
||||
return {
|
||||
getBacktestHistory,
|
||||
getBacktestHistoryResult,
|
||||
backtestHistoryList,
|
||||
timestampms,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,21 +19,29 @@
|
|||
<script lang="ts">
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { StrategyBacktestResult } from '@/types';
|
||||
import { Component, Emit, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component({})
|
||||
export default class BacktestResultSelect extends Vue {
|
||||
@Prop({ required: true }) backtestHistory!: StrategyBacktestResult[];
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
@Prop({ required: false, default: '' }) selectedBacktestResultKey!: string;
|
||||
|
||||
@Emit('selectionChange')
|
||||
setBacktestResult(key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
formatPercent = formatPercent;
|
||||
}
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -59,11 +59,9 @@
|
|||
|
||||
<script lang="ts">
|
||||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { StrategyBacktestResult, Trade } from '@/types';
|
||||
|
||||
import ValuePair from '@/components/general/ValuePair.vue';
|
||||
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import {
|
||||
timestampms,
|
||||
formatPercent,
|
||||
|
@ -71,298 +69,310 @@ import {
|
|||
humanizeDurationFromSeconds,
|
||||
} from '@/shared/formatters';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'LoginModal',
|
||||
components: {
|
||||
TradeList,
|
||||
ValuePair,
|
||||
},
|
||||
})
|
||||
export default class BacktestResultView extends Vue {
|
||||
@Prop({ required: true }) readonly backtestResult!: StrategyBacktestResult;
|
||||
props: {
|
||||
backtestResult: { required: true, type: Object as () => StrategyBacktestResult },
|
||||
},
|
||||
setup(props) {
|
||||
const hasBacktestResult = computed(() => {
|
||||
return !!props.backtestResult;
|
||||
});
|
||||
|
||||
get hasBacktestResult() {
|
||||
return !!this.backtestResult;
|
||||
}
|
||||
const formatPriceStake = (price) => {
|
||||
return `${formatPrice(price, props.backtestResult.stake_currency_decimals)} ${
|
||||
props.backtestResult.stake_currency
|
||||
}`;
|
||||
};
|
||||
const getSortedTrades = (backtestResult: StrategyBacktestResult): Trade[] => {
|
||||
const sortedTrades = backtestResult.trades
|
||||
.slice()
|
||||
.sort((a, b) => a.profit_ratio - b.profit_ratio);
|
||||
return sortedTrades;
|
||||
};
|
||||
|
||||
getSortedTrades(backtestResult: StrategyBacktestResult): Trade[] {
|
||||
const sortedTrades = backtestResult.trades
|
||||
.slice()
|
||||
.sort((a, b) => a.profit_ratio - b.profit_ratio);
|
||||
return sortedTrades;
|
||||
}
|
||||
const bestPair = computed((): string => {
|
||||
const trades = getSortedTrades(props.backtestResult);
|
||||
const value = trades[trades.length - 1];
|
||||
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
|
||||
});
|
||||
const worstPair = computed((): string => {
|
||||
const trades = getSortedTrades(props.backtestResult);
|
||||
const value = trades[0];
|
||||
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
|
||||
});
|
||||
const backtestResultStats = computed(() => {
|
||||
// Transpose Result into readable format
|
||||
const shortMetrics =
|
||||
props.backtestResult?.trade_count_short && props.backtestResult?.trade_count_short > 0
|
||||
? [
|
||||
{ metric: '___', value: '___' },
|
||||
{
|
||||
metric: 'Long / Short',
|
||||
value: `${props.backtestResult.trade_count_long} / ${props.backtestResult.trade_count_short}`,
|
||||
},
|
||||
{
|
||||
metric: 'Total profit Long',
|
||||
value: `${formatPercent(
|
||||
props.backtestResult.profit_total_long || 0,
|
||||
)} | ${formatPriceStake(props.backtestResult.profit_total_long_abs)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Total profit Short',
|
||||
value: `${formatPercent(
|
||||
props.backtestResult.profit_total_short || 0,
|
||||
)} | ${formatPriceStake(props.backtestResult.profit_total_short_abs)}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
formatPriceStake(price) {
|
||||
return `${formatPrice(price, this.backtestResult.stake_currency_decimals)} ${
|
||||
this.backtestResult.stake_currency
|
||||
}`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
metric: 'Total Profit',
|
||||
value: `${formatPercent(props.backtestResult.profit_total)} | ${formatPriceStake(
|
||||
props.backtestResult.profit_total_abs,
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Total trades / Daily Avg Trades',
|
||||
value: `${props.backtestResult.total_trades} / ${props.backtestResult.trades_per_day}`,
|
||||
},
|
||||
// { metric: 'First trade', value: props.backtestResult.backtest_fi },
|
||||
// { metric: 'First trade Pair', value: props.backtestResult.backtest_best_day },
|
||||
{
|
||||
metric: 'Best day',
|
||||
value: `${formatPercent(props.backtestResult.backtest_best_day, 2)} | ${formatPriceStake(
|
||||
props.backtestResult.backtest_best_day_abs,
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Worst day',
|
||||
value: `${formatPercent(props.backtestResult.backtest_worst_day, 2)} | ${formatPriceStake(
|
||||
props.backtestResult.backtest_worst_day_abs,
|
||||
)}`,
|
||||
},
|
||||
|
||||
get bestPair(): string {
|
||||
const trades = this.getSortedTrades(this.backtestResult);
|
||||
const value = trades[trades.length - 1];
|
||||
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
|
||||
}
|
||||
{
|
||||
metric: 'Win/Draw/Loss',
|
||||
value: `${
|
||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
||||
.wins
|
||||
} / ${
|
||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
||||
.draws
|
||||
} / ${
|
||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
||||
.losses
|
||||
}`,
|
||||
},
|
||||
{
|
||||
metric: 'Days win/draw/loss',
|
||||
value: `${props.backtestResult.winning_days} / ${props.backtestResult.draw_days} / ${props.backtestResult.losing_days}`,
|
||||
},
|
||||
|
||||
get worstPair(): string {
|
||||
const trades = this.getSortedTrades(this.backtestResult);
|
||||
const value = trades[0];
|
||||
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
|
||||
}
|
||||
{
|
||||
metric: 'Avg. Duration winners',
|
||||
value: humanizeDurationFromSeconds(props.backtestResult.winner_holding_avg),
|
||||
},
|
||||
{
|
||||
metric: 'Avg. Duration Losers',
|
||||
value: humanizeDurationFromSeconds(props.backtestResult.loser_holding_avg),
|
||||
},
|
||||
{ metric: 'Rejected entry signals', value: props.backtestResult.rejected_signals },
|
||||
{
|
||||
metric: 'Entry/Exit timeouts',
|
||||
value: `${props.backtestResult.timedout_entry_orders} / ${props.backtestResult.timedout_exit_orders}`,
|
||||
},
|
||||
|
||||
get backtestResultStats() {
|
||||
// Transpose Result into readable format
|
||||
const shortMetrics =
|
||||
this.backtestResult?.trade_count_short && this.backtestResult?.trade_count_short > 0
|
||||
? [
|
||||
{ metric: '___', value: '___' },
|
||||
{
|
||||
metric: 'Long / Short',
|
||||
value: `${this.backtestResult.trade_count_long} / ${this.backtestResult.trade_count_short}`,
|
||||
},
|
||||
{
|
||||
metric: 'Total profit Long',
|
||||
value: `${formatPercent(
|
||||
this.backtestResult.profit_total_long || 0,
|
||||
)} | ${this.formatPriceStake(this.backtestResult.profit_total_long_abs)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Total profit Short',
|
||||
value: `${formatPercent(
|
||||
this.backtestResult.profit_total_short || 0,
|
||||
)} | ${this.formatPriceStake(this.backtestResult.profit_total_short_abs)}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
...shortMetrics,
|
||||
|
||||
return [
|
||||
{
|
||||
metric: 'Total Profit',
|
||||
value: `${formatPercent(this.backtestResult.profit_total)} | ${this.formatPriceStake(
|
||||
this.backtestResult.profit_total_abs,
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Total trades / Daily Avg Trades',
|
||||
value: `${this.backtestResult.total_trades} / ${this.backtestResult.trades_per_day}`,
|
||||
},
|
||||
// { metric: 'First trade', value: this.backtestResult.backtest_fi },
|
||||
// { metric: 'First trade Pair', value: this.backtestResult.backtest_best_day },
|
||||
{
|
||||
metric: 'Best day',
|
||||
value: `${formatPercent(
|
||||
this.backtestResult.backtest_best_day,
|
||||
2,
|
||||
)} | ${this.formatPriceStake(this.backtestResult.backtest_best_day_abs)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Worst day',
|
||||
value: `${formatPercent(
|
||||
this.backtestResult.backtest_worst_day,
|
||||
2,
|
||||
)} | ${this.formatPriceStake(this.backtestResult.backtest_worst_day_abs)}`,
|
||||
},
|
||||
{ metric: '___', value: '___' },
|
||||
{ metric: 'Min balance', value: formatPriceStake(props.backtestResult.csum_min) },
|
||||
{ metric: 'Max balance', value: formatPriceStake(props.backtestResult.csum_max) },
|
||||
{ metric: 'Market change', value: formatPercent(props.backtestResult.market_change) },
|
||||
{ metric: '___', value: '___' },
|
||||
{
|
||||
metric: 'Max Drawdown (Account)',
|
||||
value: formatPercent(props.backtestResult.max_drawdown_account),
|
||||
},
|
||||
{
|
||||
metric: 'Max Drawdown ABS',
|
||||
value: formatPriceStake(props.backtestResult.max_drawdown_abs),
|
||||
},
|
||||
{
|
||||
metric: 'Drawdown high | low',
|
||||
value: `${formatPriceStake(props.backtestResult.max_drawdown_high)} | ${formatPriceStake(
|
||||
props.backtestResult.max_drawdown_low,
|
||||
)}`,
|
||||
},
|
||||
{ metric: 'Drawdown start', value: timestampms(props.backtestResult.drawdown_start_ts) },
|
||||
{ metric: 'Drawdown end', value: timestampms(props.backtestResult.drawdown_end_ts) },
|
||||
{ metric: '___', value: '___' },
|
||||
|
||||
{
|
||||
metric: 'Win/Draw/Loss',
|
||||
value: `${
|
||||
this.backtestResult.results_per_pair[this.backtestResult.results_per_pair.length - 1].wins
|
||||
} / ${
|
||||
this.backtestResult.results_per_pair[this.backtestResult.results_per_pair.length - 1]
|
||||
.draws
|
||||
} / ${
|
||||
this.backtestResult.results_per_pair[this.backtestResult.results_per_pair.length - 1]
|
||||
.losses
|
||||
}`,
|
||||
},
|
||||
{
|
||||
metric: 'Days win/draw/loss',
|
||||
value: `${this.backtestResult.winning_days} / ${this.backtestResult.draw_days} / ${this.backtestResult.losing_days}`,
|
||||
},
|
||||
{
|
||||
metric: 'Best Pair',
|
||||
value: `${props.backtestResult.best_pair.key} ${formatPercent(
|
||||
props.backtestResult.best_pair.profit_sum,
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Worst Pair',
|
||||
value: `${props.backtestResult.worst_pair.key} ${formatPercent(
|
||||
props.backtestResult.worst_pair.profit_sum,
|
||||
)}`,
|
||||
},
|
||||
{ metric: 'Best single Trade', value: bestPair },
|
||||
{ metric: 'Worst single Trade', value: worstPair },
|
||||
];
|
||||
});
|
||||
|
||||
{
|
||||
metric: 'Avg. Duration winners',
|
||||
value: humanizeDurationFromSeconds(this.backtestResult.winner_holding_avg),
|
||||
},
|
||||
{
|
||||
metric: 'Avg. Duration Losers',
|
||||
value: humanizeDurationFromSeconds(this.backtestResult.loser_holding_avg),
|
||||
},
|
||||
{ metric: 'Rejected entry signals', value: this.backtestResult.rejected_signals },
|
||||
{
|
||||
metric: 'Entry/Exit timeouts',
|
||||
value: `${this.backtestResult.timedout_entry_orders} / ${this.backtestResult.timedout_exit_orders}`,
|
||||
},
|
||||
const backtestResultSettings = computed(() => {
|
||||
// Transpose Result into readable format
|
||||
return [
|
||||
{ setting: 'Backtesting from', value: timestampms(props.backtestResult.backtest_start_ts) },
|
||||
{ setting: 'Backtesting to', value: timestampms(props.backtestResult.backtest_end_ts) },
|
||||
{
|
||||
setting: 'BT execution time',
|
||||
value: humanizeDurationFromSeconds(
|
||||
props.backtestResult.backtest_run_end_ts - props.backtestResult.backtest_run_start_ts,
|
||||
),
|
||||
},
|
||||
{ setting: 'Max open trades', value: props.backtestResult.max_open_trades },
|
||||
{ setting: 'Timeframe', value: props.backtestResult.timeframe },
|
||||
{ setting: 'Timerange', value: props.backtestResult.timerange },
|
||||
{ setting: 'Stoploss', value: formatPercent(props.backtestResult.stoploss, 2) },
|
||||
{ setting: 'Trailing Stoploss', value: props.backtestResult.trailing_stop },
|
||||
{
|
||||
setting: 'Trail only when offset is reached',
|
||||
value: props.backtestResult.trailing_only_offset_is_reached,
|
||||
},
|
||||
{ setting: 'Trailing Stop positive', value: props.backtestResult.trailing_stop_positive },
|
||||
{
|
||||
setting: 'Trailing stop positive offset',
|
||||
value: props.backtestResult.trailing_stop_positive_offset,
|
||||
},
|
||||
{ setting: 'Custom Stoploss', value: props.backtestResult.use_custom_stoploss },
|
||||
{ setting: 'ROI', value: props.backtestResult.minimal_roi },
|
||||
{
|
||||
setting: 'Use Exit Signal',
|
||||
value:
|
||||
props.backtestResult.use_exit_signal !== undefined
|
||||
? props.backtestResult.use_exit_signal
|
||||
: props.backtestResult.use_sell_signal,
|
||||
},
|
||||
{
|
||||
setting: 'Exit profit only',
|
||||
value:
|
||||
props.backtestResult.exit_profit_only !== undefined
|
||||
? props.backtestResult.exit_profit_only
|
||||
: props.backtestResult.sell_profit_only,
|
||||
},
|
||||
{
|
||||
setting: 'Exit profit offset',
|
||||
value:
|
||||
props.backtestResult.exit_profit_offset !== undefined
|
||||
? props.backtestResult.exit_profit_offset
|
||||
: props.backtestResult.sell_profit_offset,
|
||||
},
|
||||
{ setting: 'Enable protections', value: props.backtestResult.enable_protections },
|
||||
{
|
||||
setting: 'Starting balance',
|
||||
value: formatPriceStake(props.backtestResult.starting_balance),
|
||||
},
|
||||
{
|
||||
setting: 'Final balance',
|
||||
value: formatPriceStake(props.backtestResult.final_balance),
|
||||
},
|
||||
{
|
||||
setting: 'Avg. stake amount',
|
||||
value: formatPriceStake(props.backtestResult.avg_stake_amount),
|
||||
},
|
||||
{
|
||||
setting: 'Total trade volume',
|
||||
value: formatPriceStake(props.backtestResult.total_volume),
|
||||
},
|
||||
];
|
||||
});
|
||||
const perPairFields = computed(() => {
|
||||
return [
|
||||
{ key: 'key', label: 'Pair' },
|
||||
{ key: 'trades', label: 'Buys' },
|
||||
{
|
||||
key: 'profit_mean',
|
||||
label: 'Avg Profit %',
|
||||
formatter: (value) => formatPercent(value, 2),
|
||||
},
|
||||
{ key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) },
|
||||
{
|
||||
key: 'profit_total_abs',
|
||||
label: `Tot Profit ${props.backtestResult.stake_currency}`,
|
||||
formatter: (value) => formatPrice(value, props.backtestResult.stake_currency_decimals),
|
||||
},
|
||||
{
|
||||
key: 'profit_total',
|
||||
label: 'Tot Profit %',
|
||||
formatter: (value) => formatPercent(value, 2),
|
||||
},
|
||||
{ key: 'duration_avg', label: 'Avg Duration' },
|
||||
{ key: 'wins', label: 'Wins' },
|
||||
{ key: 'draws', label: 'Draws' },
|
||||
{ key: 'losses', label: 'Losses' },
|
||||
];
|
||||
});
|
||||
|
||||
...shortMetrics,
|
||||
const perExitReason = computed(() => {
|
||||
return [
|
||||
{ key: 'exit_reason', label: 'Exit Reason' },
|
||||
{ key: 'trades', label: 'Buys' },
|
||||
{
|
||||
key: 'profit_mean',
|
||||
label: 'Avg Profit %',
|
||||
formatter: (value) => formatPercent(value, 2),
|
||||
},
|
||||
{ key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) },
|
||||
{
|
||||
key: 'profit_total_abs',
|
||||
label: `Tot Profit ${props.backtestResult.stake_currency}`,
|
||||
|
||||
{ metric: '___', value: '___' },
|
||||
{ metric: 'Min balance', value: this.formatPriceStake(this.backtestResult.csum_min) },
|
||||
{ metric: 'Max balance', value: this.formatPriceStake(this.backtestResult.csum_max) },
|
||||
{ metric: 'Market change', value: formatPercent(this.backtestResult.market_change) },
|
||||
{ metric: '___', value: '___' },
|
||||
{
|
||||
metric: 'Max Drawdown (Account)',
|
||||
value: formatPercent(this.backtestResult.max_drawdown_account),
|
||||
},
|
||||
{
|
||||
metric: 'Max Drawdown ABS',
|
||||
value: this.formatPriceStake(this.backtestResult.max_drawdown_abs),
|
||||
},
|
||||
{
|
||||
metric: 'Drawdown high | low',
|
||||
value: `${this.formatPriceStake(
|
||||
this.backtestResult.max_drawdown_high,
|
||||
)} | ${this.formatPriceStake(this.backtestResult.max_drawdown_low)}`,
|
||||
},
|
||||
{ metric: 'Drawdown start', value: timestampms(this.backtestResult.drawdown_start_ts) },
|
||||
{ metric: 'Drawdown end', value: timestampms(this.backtestResult.drawdown_end_ts) },
|
||||
{ metric: '___', value: '___' },
|
||||
|
||||
{
|
||||
metric: 'Best Pair',
|
||||
value: `${this.backtestResult.best_pair.key} ${formatPercent(
|
||||
this.backtestResult.best_pair.profit_sum,
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
metric: 'Worst Pair',
|
||||
value: `${this.backtestResult.worst_pair.key} ${formatPercent(
|
||||
this.backtestResult.worst_pair.profit_sum,
|
||||
)}`,
|
||||
},
|
||||
{ metric: 'Best single Trade', value: this.bestPair },
|
||||
{ metric: 'Worst single Trade', value: this.worstPair },
|
||||
formatter: (value) => formatPrice(value, props.backtestResult.stake_currency_decimals),
|
||||
},
|
||||
{
|
||||
key: 'profit_total',
|
||||
label: 'Tot Profit %',
|
||||
formatter: (value) => formatPercent(value, 2),
|
||||
},
|
||||
{ key: 'wins', label: 'Wins' },
|
||||
{ key: 'draws', label: 'Draws' },
|
||||
{ key: 'losses', label: 'Losses' },
|
||||
];
|
||||
});
|
||||
const backtestResultFields: Array<Record<string, string>> = [
|
||||
{ key: 'metric', label: 'Metric' },
|
||||
{ key: 'value', label: 'Value' },
|
||||
];
|
||||
}
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
get backtestResultSettings() {
|
||||
// Transpose Result into readable format
|
||||
return [
|
||||
{ setting: 'Backtesting from', value: timestampms(this.backtestResult.backtest_start_ts) },
|
||||
{ setting: 'Backtesting to', value: timestampms(this.backtestResult.backtest_end_ts) },
|
||||
{
|
||||
setting: 'BT execution time',
|
||||
value: humanizeDurationFromSeconds(
|
||||
this.backtestResult.backtest_run_end_ts - this.backtestResult.backtest_run_start_ts,
|
||||
),
|
||||
},
|
||||
{ setting: 'Max open trades', value: this.backtestResult.max_open_trades },
|
||||
{ setting: 'Timeframe', value: this.backtestResult.timeframe },
|
||||
{ setting: 'Timerange', value: this.backtestResult.timerange },
|
||||
{ setting: 'Stoploss', value: formatPercent(this.backtestResult.stoploss, 2) },
|
||||
{ setting: 'Trailing Stoploss', value: this.backtestResult.trailing_stop },
|
||||
{
|
||||
setting: 'Trail only when offset is reached',
|
||||
value: this.backtestResult.trailing_only_offset_is_reached,
|
||||
},
|
||||
{ setting: 'Trailing Stop positive', value: this.backtestResult.trailing_stop_positive },
|
||||
{
|
||||
setting: 'Trailing stop positive offset',
|
||||
value: this.backtestResult.trailing_stop_positive_offset,
|
||||
},
|
||||
{ setting: 'Custom Stoploss', value: this.backtestResult.use_custom_stoploss },
|
||||
{ setting: 'ROI', value: this.backtestResult.minimal_roi },
|
||||
{
|
||||
setting: 'Use Exit Signal',
|
||||
value:
|
||||
this.backtestResult.use_exit_signal !== undefined
|
||||
? this.backtestResult.use_exit_signal
|
||||
: this.backtestResult.use_sell_signal,
|
||||
},
|
||||
{
|
||||
setting: 'Exit profit only',
|
||||
value:
|
||||
this.backtestResult.exit_profit_only !== undefined
|
||||
? this.backtestResult.exit_profit_only
|
||||
: this.backtestResult.sell_profit_only,
|
||||
},
|
||||
{
|
||||
setting: 'Exit profit offset',
|
||||
value:
|
||||
this.backtestResult.exit_profit_offset !== undefined
|
||||
? this.backtestResult.exit_profit_offset
|
||||
: this.backtestResult.sell_profit_offset,
|
||||
},
|
||||
{ setting: 'Enable protections', value: this.backtestResult.enable_protections },
|
||||
{
|
||||
setting: 'Starting balance',
|
||||
value: this.formatPriceStake(this.backtestResult.starting_balance),
|
||||
},
|
||||
{
|
||||
setting: 'Final balance',
|
||||
value: this.formatPriceStake(this.backtestResult.final_balance),
|
||||
},
|
||||
{
|
||||
setting: 'Avg. stake amount',
|
||||
value: this.formatPriceStake(this.backtestResult.avg_stake_amount),
|
||||
},
|
||||
{
|
||||
setting: 'Total trade volume',
|
||||
value: this.formatPriceStake(this.backtestResult.total_volume),
|
||||
},
|
||||
const backtestsettingFields: Array<Record<string, string>> = [
|
||||
{ key: 'setting', label: 'Setting' },
|
||||
{ key: 'value', label: 'Value' },
|
||||
];
|
||||
}
|
||||
|
||||
get perPairFields() {
|
||||
return [
|
||||
{ key: 'key', label: 'Pair' },
|
||||
{ key: 'trades', label: 'Buys' },
|
||||
{ key: 'profit_mean', label: 'Avg Profit %', formatter: (value) => formatPercent(value, 2) },
|
||||
{ key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) },
|
||||
{
|
||||
key: 'profit_total_abs',
|
||||
label: `Tot Profit ${this.backtestResult.stake_currency}`,
|
||||
formatter: (value) => formatPrice(value, this.backtestResult.stake_currency_decimals),
|
||||
},
|
||||
{
|
||||
key: 'profit_total',
|
||||
label: 'Tot Profit %',
|
||||
formatter: (value) => formatPercent(value, 2),
|
||||
},
|
||||
{ key: 'duration_avg', label: 'Avg Duration' },
|
||||
{ key: 'wins', label: 'Wins' },
|
||||
{ key: 'draws', label: 'Draws' },
|
||||
{ key: 'losses', label: 'Losses' },
|
||||
];
|
||||
}
|
||||
|
||||
get perExitReason() {
|
||||
return [
|
||||
{ key: 'exit_reason', label: 'Exit Reason' },
|
||||
{ key: 'trades', label: 'Buys' },
|
||||
{ key: 'profit_mean', label: 'Avg Profit %', formatter: (value) => formatPercent(value, 2) },
|
||||
{ key: 'profit_sum', label: 'Cum Profit %', formatter: (value) => formatPercent(value, 2) },
|
||||
{
|
||||
key: 'profit_total_abs',
|
||||
label: `Tot Profit ${this.backtestResult.stake_currency}`,
|
||||
|
||||
formatter: (value) => formatPrice(value, this.backtestResult.stake_currency_decimals),
|
||||
},
|
||||
{
|
||||
key: 'profit_total',
|
||||
label: 'Tot Profit %',
|
||||
formatter: (value) => formatPercent(value, 2),
|
||||
},
|
||||
{ key: 'wins', label: 'Wins' },
|
||||
{ key: 'draws', label: 'Draws' },
|
||||
{ key: 'losses', label: 'Losses' },
|
||||
];
|
||||
}
|
||||
|
||||
backtestResultFields: Array<Record<string, string>> = [
|
||||
{ key: 'metric', label: 'Metric' },
|
||||
{ key: 'value', label: 'Value' },
|
||||
];
|
||||
|
||||
backtestsettingFields: Array<Record<string, string>> = [
|
||||
{ key: 'setting', label: 'Setting' },
|
||||
{ key: 'value', label: 'Value' },
|
||||
];
|
||||
}
|
||||
return {
|
||||
hasBacktestResult,
|
||||
formatPriceStake,
|
||||
bestPair,
|
||||
worstPair,
|
||||
backtestResultStats,
|
||||
backtestResultSettings,
|
||||
perPairFields,
|
||||
perExitReason,
|
||||
backtestResultFields,
|
||||
backtestsettingFields,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div>
|
||||
<div class="mb-2">
|
||||
<label class="mr-auto h3">Balance</label>
|
||||
<b-button class="float-right" size="sm" @click="getBalance">↻</b-button>
|
||||
<b-button class="float-right" size="sm" @click="botStore.activeBot.getBalance"
|
||||
>↻</b-button
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-model="hideSmallBalances"
|
||||
class="float-right"
|
||||
|
@ -16,20 +18,20 @@
|
|||
</div>
|
||||
<BalanceChart v-if="balanceCurrencies" :currencies="balanceCurrencies" />
|
||||
<div>
|
||||
<p v-if="balance.note">
|
||||
<strong>{{ balance.note }}</strong>
|
||||
<p v-if="botStore.activeBot.balance.note">
|
||||
<strong>{{ botStore.activeBot.balance.note }}</strong>
|
||||
</p>
|
||||
<b-table class="table-sm" :items="balanceCurrencies" :fields="tableFields">
|
||||
<template slot="bottom-row">
|
||||
<td><strong>Total</strong></td>
|
||||
<td>
|
||||
<span class="font-italic" title="Increase over initial capital">{{
|
||||
formatPercent(balance.starting_capital_ratio)
|
||||
formatPercent(botStore.activeBot.balance.starting_capital_ratio)
|
||||
}}</span>
|
||||
</td>
|
||||
<!-- this is a computed prop that adds up all the expenses in the visible rows -->
|
||||
<td>
|
||||
<strong>{{ formatCurrency(balance.total) }}</strong>
|
||||
<strong>{{ formatCurrency(botStore.activeBot.balance.total) }}</strong>
|
||||
</td>
|
||||
</template>
|
||||
</b-table>
|
||||
|
@ -38,56 +40,59 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BalanceInterface } from '@/types';
|
||||
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 { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Balance',
|
||||
components: { HideIcon, ShowIcon, BalanceChart },
|
||||
})
|
||||
export default class Balance extends Vue {
|
||||
@ftbot.Action getBalance;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const hideSmallBalances = ref(true);
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.balance]!: BalanceInterface;
|
||||
const smallBalance = computed((): number => {
|
||||
return Number((0.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8));
|
||||
});
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrencyDecimals]!: number;
|
||||
const balanceCurrencies = computed(() => {
|
||||
if (!hideSmallBalances.value) {
|
||||
return botStore.activeBot.balance.currencies;
|
||||
}
|
||||
|
||||
hideSmallBalances = true;
|
||||
formatPercent = formatPercent;
|
||||
return botStore.activeBot.balance.currencies?.filter(
|
||||
(v) => v.est_stake >= smallBalance.value,
|
||||
);
|
||||
});
|
||||
|
||||
get smallBalance(): number {
|
||||
return Number((0.1 ** this.stakeCurrencyDecimals).toFixed(8));
|
||||
}
|
||||
const tableFields = computed(() => {
|
||||
return [
|
||||
{ key: 'currency', label: 'Currency' },
|
||||
{ key: 'free', label: 'Available', formatter: 'formatCurrency' },
|
||||
{
|
||||
key: 'est_stake',
|
||||
label: `in ${botStore.activeBot.balance.stake}`,
|
||||
formatter: 'formatCurrency',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
get balanceCurrencies() {
|
||||
if (!this.hideSmallBalances) {
|
||||
return this.balance.currencies;
|
||||
}
|
||||
const formatCurrency = (value) => {
|
||||
return value ? value.toFixed(5) : '';
|
||||
};
|
||||
|
||||
return this.balance?.currencies?.filter((v) => v.est_stake >= this.smallBalance);
|
||||
}
|
||||
|
||||
get tableFields() {
|
||||
return [
|
||||
{ key: 'currency', label: 'Currency' },
|
||||
{ key: 'free', label: 'Available', formatter: 'formatCurrency' },
|
||||
{ key: 'est_stake', label: `in ${this.balance.stake}`, formatter: 'formatCurrency' },
|
||||
];
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.getBalance();
|
||||
}
|
||||
|
||||
formatCurrency(value) {
|
||||
return value ? value.toFixed(5) : '';
|
||||
}
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
hideSmallBalances,
|
||||
formatPercent,
|
||||
smallBalance,
|
||||
balanceCurrencies,
|
||||
tableFields,
|
||||
formatCurrency,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="bot-alerts">
|
||||
<b-alert
|
||||
v-for="(alert, index) in activeMessages"
|
||||
v-for="(alert, index) in alertStore.activeMessages"
|
||||
:key="index"
|
||||
variant="warning"
|
||||
dismissible
|
||||
:show="5"
|
||||
:value="!!alert.message"
|
||||
@dismissed="closeAlert"
|
||||
@dismissed="alertStore.removeAlert"
|
||||
>
|
||||
{{ alert.message }}
|
||||
</b-alert>
|
||||
|
@ -15,22 +15,16 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { AlertActions } from '@/store/modules/alerts';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { AlertType } from '@/types/alertTypes';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { useAlertsStore } from '@/stores/alerts';
|
||||
|
||||
const alerts = namespace(StoreModules.alerts);
|
||||
|
||||
@Component({})
|
||||
export default class BotAlerts extends Vue {
|
||||
@alerts.State activeMessages!: AlertType[];
|
||||
|
||||
@alerts.Action [AlertActions.removeAlert];
|
||||
|
||||
closeAlert() {
|
||||
this[AlertActions.removeAlert]();
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'BotAlerts',
|
||||
setup() {
|
||||
const alertStore = useAlertsStore();
|
||||
return {
|
||||
alertStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -43,87 +43,82 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import { BalanceInterface, BotDescriptors, BotState, ProfitInterface, Trade } from '@/types';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
export default defineComponent({
|
||||
name: 'BotComparisonList',
|
||||
components: { ProfitPill },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Component({ components: { ProfitPill } })
|
||||
export default class BotComparisonList extends Vue {
|
||||
@ftbot.Getter [MultiBotStoreGetters.allProfit]!: Record<string, ProfitInterface>;
|
||||
const tableFields: Record<string, string | Function>[] = [
|
||||
{ key: 'botId', 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' },
|
||||
];
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allOpenTradeCount]!: Record<string, number>;
|
||||
const tableItems = computed(() => {
|
||||
console.log('tableItems called');
|
||||
const val: any[] = [];
|
||||
const summary = {
|
||||
botId: 'Summary',
|
||||
profitClosed: 0,
|
||||
profitClosedRatio: 0,
|
||||
profitOpen: 0,
|
||||
profitOpenRatio: 0,
|
||||
stakeCurrency: 'USDT',
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allOpenTrades]!: Record<string, Trade[]>;
|
||||
Object.entries(botStore.allProfit).forEach(([k, v]: [k: string, v: any]) => {
|
||||
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);
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allBotState]!: Record<string, BotState>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allBalance]!: Record<string, BalanceInterface>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAvailableBots]!: BotDescriptors;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
get tableItems() {
|
||||
console.log('tableItems called');
|
||||
const val: any[] = [];
|
||||
const summary = {
|
||||
botId: 'Summary',
|
||||
profitClosed: 0,
|
||||
profitClosedRatio: 0,
|
||||
profitOpen: 0,
|
||||
profitOpenRatio: 0,
|
||||
stakeCurrency: 'USDT',
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
|
||||
Object.entries(this.allProfit).forEach(([k, v]) => {
|
||||
const allStakes = this.allOpenTrades[k].reduce((a, b) => a + b.stake_amount, 0);
|
||||
const profitOpenRatio =
|
||||
this.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) / allStakes;
|
||||
const profitOpen = this.allOpenTrades[k].reduce((a, b) => a + b.profit_abs, 0);
|
||||
|
||||
// TODO: handle one inactive bot ...
|
||||
val.push({
|
||||
botId: this.allAvailableBots[k].botName,
|
||||
trades: `${this.allOpenTradeCount[k]} / ${this.allBotState[k]?.max_open_trades || 'N/A'}`,
|
||||
profitClosed: v.profit_closed_coin,
|
||||
profitClosedRatio: v.profit_closed_ratio || 0,
|
||||
stakeCurrency: this.allBotState[k]?.stake_currency || '',
|
||||
profitOpenRatio,
|
||||
profitOpen,
|
||||
wins: v.winning_trades,
|
||||
losses: v.losing_trades,
|
||||
balance: this.allBalance[k]?.total,
|
||||
stakeCurrencyDecimals: this.allBotState[k]?.stake_currency_decimals || 3,
|
||||
// TODO: handle one inactive bot ...
|
||||
val.push({
|
||||
botId: 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;
|
||||
}
|
||||
});
|
||||
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;
|
||||
});
|
||||
val.push(summary);
|
||||
return val;
|
||||
}
|
||||
|
||||
tableFields: Record<string, string | Function>[] = [
|
||||
{ key: 'botId', 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' },
|
||||
];
|
||||
}
|
||||
return {
|
||||
formatPrice,
|
||||
tableFields,
|
||||
tableItems,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
forceexit
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || isRunning"
|
||||
:disabled="!botStore.activeBot.isTrading || isRunning"
|
||||
title="Start Trading"
|
||||
@click="startBot()"
|
||||
>
|
||||
|
@ -10,7 +11,7 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
:disabled="!botStore.activeBot.isTrading || !isRunning"
|
||||
title="Stop Trading - Also stops handling open trades."
|
||||
@click="handleStopBot()"
|
||||
>
|
||||
|
@ -18,7 +19,7 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
:disabled="!botStore.activeBot.isTrading || !isRunning"
|
||||
title="StopBuy - Stops buying, but still handles open trades"
|
||||
@click="handleStopBuy()"
|
||||
>
|
||||
|
@ -26,7 +27,7 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading"
|
||||
:disabled="!botStore.activeBot.isTrading"
|
||||
title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly."
|
||||
@click="handleReloadConfig()"
|
||||
>
|
||||
|
@ -34,23 +35,27 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading"
|
||||
title="Forcesell all"
|
||||
@click="handleForceSell()"
|
||||
:disabled="!botStore.activeBot.isTrading"
|
||||
title="Force exit all"
|
||||
@click="handleForceExit()"
|
||||
>
|
||||
<ForceSellIcon />
|
||||
<ForceExitIcon />
|
||||
</button>
|
||||
<button
|
||||
v-if="botState && (botState.force_entry_enable || botState.forcebuy_enabled)"
|
||||
v-if="
|
||||
botStore.activeBot.botState &&
|
||||
(botStore.activeBot.botState.force_entry_enable ||
|
||||
botStore.activeBot.botState.forcebuy_enabled)
|
||||
"
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="Force enter - Immediately buy an asset at an optional price. Sells are then handled according to strategy rules."
|
||||
: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="initiateForceenter"
|
||||
>
|
||||
<ForceBuyIcon />
|
||||
<ForceEntryIcon />
|
||||
</button>
|
||||
<button
|
||||
v-if="isWebserverMode && false"
|
||||
v-if="botStore.activeBot.isWebserverMode && false"
|
||||
:disabled="isTrading"
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
title="Start Trading mode"
|
||||
|
@ -63,98 +68,87 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BotState, ForceSellPayload } from '@/types';
|
||||
import { BotStoreActions, BotStoreGetters } from '@/store/modules/ftbot';
|
||||
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 ForceSellIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
|
||||
import ForceBuyIcon from 'vue-material-design-icons/PlusBoxMultipleOutline.vue';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import ForceExitIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
|
||||
import ForceEntryIcon from 'vue-material-design-icons/PlusBoxMultipleOutline.vue';
|
||||
import ForceBuyForm from './ForceBuyForm.vue';
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'BotControls',
|
||||
components: {
|
||||
ForceBuyForm,
|
||||
PlayIcon,
|
||||
StopIcon,
|
||||
PauseIcon,
|
||||
ReloadIcon,
|
||||
ForceSellIcon,
|
||||
ForceBuyIcon,
|
||||
ForceExitIcon,
|
||||
ForceEntryIcon,
|
||||
},
|
||||
})
|
||||
export default class BotControls extends Vue {
|
||||
forcebuyShow = false;
|
||||
setup(_, { root }) {
|
||||
const botStore = useBotStore();
|
||||
const forcebuyShow = ref(false);
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botState]?: BotState;
|
||||
|
||||
@ftbot.Action startBot;
|
||||
|
||||
@ftbot.Action stopBot;
|
||||
|
||||
@ftbot.Action stopBuy;
|
||||
|
||||
@ftbot.Action reloadConfig;
|
||||
|
||||
@ftbot.Action startTrade;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action [BotStoreActions.forcesell]!: (payload: ForceSellPayload) => void;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.isTrading]!: boolean;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.isWebserverMode]!: boolean;
|
||||
|
||||
get isRunning(): boolean {
|
||||
return this.botState?.state === 'running';
|
||||
}
|
||||
|
||||
initiateForceenter() {
|
||||
this.$bvModal.show('forcebuy-modal');
|
||||
}
|
||||
|
||||
handleStopBot() {
|
||||
this.$bvModal.msgBoxConfirm('Stop Bot?').then((value: boolean) => {
|
||||
if (value) {
|
||||
this.stopBot();
|
||||
}
|
||||
const isRunning = computed((): boolean => {
|
||||
return botStore.activeBot.botState?.state === 'running';
|
||||
});
|
||||
}
|
||||
|
||||
handleStopBuy() {
|
||||
this.$bvModal
|
||||
.msgBoxConfirm('Stop buying? Freqtrade will continue to handle open trades.')
|
||||
.then((value: boolean) => {
|
||||
const initiateForceenter = () => {
|
||||
root.$bvModal.show('forcebuy-modal');
|
||||
};
|
||||
|
||||
const handleStopBot = () => {
|
||||
root.$bvModal.msgBoxConfirm('Stop Bot?').then((value: boolean) => {
|
||||
if (value) {
|
||||
this.stopBuy();
|
||||
botStore.activeBot.stopBot();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleReloadConfig() {
|
||||
this.$bvModal.msgBoxConfirm('Reload configuration?').then((value: boolean) => {
|
||||
if (value) {
|
||||
this.reloadConfig();
|
||||
}
|
||||
});
|
||||
}
|
||||
const handleStopBuy = () => {
|
||||
root.$bvModal
|
||||
.msgBoxConfirm('Stop buying? Freqtrade will continue to handle open trades.')
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
botStore.activeBot.stopBuy();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleForceSell() {
|
||||
this.$bvModal.msgBoxConfirm(`Really forcesell ALL trades?`).then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: ForceSellPayload = {
|
||||
tradeid: 'all',
|
||||
// TODO: support ordertype (?)
|
||||
};
|
||||
this.forcesell(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const handleReloadConfig = () => {
|
||||
root.$bvModal.msgBoxConfirm('Reload configuration?').then((value: boolean) => {
|
||||
if (value) {
|
||||
botStore.activeBot.reloadConfig();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleForceExit = () => {
|
||||
root.$bvModal.msgBoxConfirm(`Really forcesell ALL trades?`).then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: ForceSellPayload = {
|
||||
tradeid: 'all',
|
||||
// TODO: support ordertype (?)
|
||||
};
|
||||
botStore.activeBot.forceexit(payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
initiateForceenter,
|
||||
handleStopBot,
|
||||
handleStopBuy,
|
||||
handleReloadConfig,
|
||||
handleForceExit,
|
||||
forcebuyShow,
|
||||
botStore,
|
||||
isRunning,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,63 +1,72 @@
|
|||
<template>
|
||||
<div v-if="botState">
|
||||
<div v-if="botStore.activeBot.botState">
|
||||
<p>
|
||||
Running Freqtrade <strong>{{ version }}</strong>
|
||||
Running Freqtrade <strong>{{ botStore.activeBot.version }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
Running with
|
||||
<strong>
|
||||
{{ botState.max_open_trades }}x{{ botState.stake_amount }} {{ botState.stake_currency }}
|
||||
{{ botStore.activeBot.botState.max_open_trades }}x{{
|
||||
botStore.activeBot.botState.stake_amount
|
||||
}}
|
||||
{{ botStore.activeBot.botState.stake_currency }}
|
||||
</strong>
|
||||
on
|
||||
<strong>{{ botState.exchange }}</strong> in
|
||||
<strong>{{ botState.trading_mode || 'spot' }}</strong> markets, with Strategy
|
||||
<strong>{{ botState.strategy }}</strong>
|
||||
<strong>{{ botStore.activeBot.botState.exchange }}</strong> in
|
||||
<strong>{{ botStore.activeBot.botState.trading_mode || 'spot' }}</strong> markets, with
|
||||
Strategy
|
||||
<strong>{{ botStore.activeBot.botState.strategy }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
Currently <strong>{{ botState.state }}</strong
|
||||
Currently <strong>{{ botStore.activeBot.botState.state }}</strong
|
||||
>,
|
||||
<strong>force entry: {{ botState.force_entry_enable || botState.forcebuy_enabled }}</strong>
|
||||
<strong
|
||||
>force entry:
|
||||
{{ botStore.activeBot.botState.force_entry_enable || botState.forcebuy_enabled }}</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ botState.dry_run ? 'Dry-Run' : 'Live' }}</strong>
|
||||
<strong>{{ botStore.activeBot.botState.dry_run ? 'Dry-Run' : 'Live' }}</strong>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
Avg Profit {{ formatPercent(profit.profit_all_ratio_mean) }} (∑
|
||||
{{ formatPercent(profit.profit_all_ratio_sum) }}) in {{ profit.trade_count }} Trades, with an
|
||||
average duration of {{ profit.avg_duration }}. Best pair: {{ profit.best_pair }}.
|
||||
Avg Profit {{ formatPercent(botStore.activeBot.profit.profit_all_ratio_mean) }} (∑
|
||||
{{ formatPercent(botStore.activeBot.profit.profit_all_ratio_sum) }}) in
|
||||
{{ botStore.activeBot.profit.trade_count }} Trades, with an average duration of
|
||||
{{ botStore.activeBot.profit.avg_duration }}. Best pair:
|
||||
{{ botStore.activeBot.profit.best_pair }}.
|
||||
</p>
|
||||
<p v-if="profit.first_trade_timestamp">
|
||||
<p v-if="botStore.activeBot.profit.first_trade_timestamp">
|
||||
First trade opened:
|
||||
|
||||
<strong><DateTimeTZ :date="profit.first_trade_timestamp" show-timezone /></strong>
|
||||
<strong
|
||||
><DateTimeTZ :date="botStore.activeBot.profit.first_trade_timestamp" show-timezone
|
||||
/></strong>
|
||||
<br />
|
||||
Last trade opened:
|
||||
<strong><DateTimeTZ :date="profit.latest_trade_timestamp" show-timezone /></strong>
|
||||
<strong
|
||||
><DateTimeTZ :date="botStore.activeBot.profit.latest_trade_timestamp" show-timezone
|
||||
/></strong>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BotState, ProfitInterface } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
@Component({ components: { DateTimeTZ } })
|
||||
export default class BotStatus extends Vue {
|
||||
@ftbot.Getter [BotStoreGetters.version]: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.profit]: ProfitInterface | {};
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botState]?: BotState;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'BotStatus',
|
||||
components: { DateTimeTZ },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
return {
|
||||
formatPercent,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -34,136 +34,104 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import { MultiDeletePayload, MultiForcesellPayload, Trade } from '@/types';
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import ForceSellIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
|
||||
import ActionIcon from 'vue-material-design-icons/GestureTap.vue';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import CustomTradeListEntry from '@/components/ftbot/CustomTradeListEntry.vue';
|
||||
import TradeProfit from './TradeProfit.vue';
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'CustomTradeList',
|
||||
components: {
|
||||
DeleteIcon,
|
||||
ForceSellIcon,
|
||||
ActionIcon,
|
||||
DateTimeTZ,
|
||||
TradeProfit,
|
||||
CustomTradeListEntry,
|
||||
},
|
||||
})
|
||||
export default class CustomTradeList extends Vue {
|
||||
$refs!: {
|
||||
tradesTable: HTMLFormElement;
|
||||
};
|
||||
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, { root }) {
|
||||
const botStore = useBotStore();
|
||||
const currentPage = ref(1);
|
||||
const filterText = ref('');
|
||||
const perPage = props.activeTrades ? 200 : 25;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
const rows = computed(() => props.trades.length);
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
@Prop({ required: true }) trades!: Array<Trade>;
|
||||
|
||||
@Prop({ default: 'Trades' }) title!: string;
|
||||
|
||||
@Prop({ required: false, default: '' }) stakeCurrency!: string;
|
||||
|
||||
@Prop({ default: false }) activeTrades!: boolean;
|
||||
|
||||
@Prop({ default: false }) showFilter!: boolean;
|
||||
|
||||
@Prop({ default: false, type: Boolean }) multiBotView!: boolean;
|
||||
|
||||
@Prop({ default: 'No Trades to show.' }) emptyText!: string;
|
||||
|
||||
@Prop({ default: 3, type: Number }) stakeCurrencyDecimals!: number;
|
||||
|
||||
@ftbot.Action setDetailTrade;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action forceSellMulti!: (payload: MultiForcesellPayload) => Promise<string>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action deleteTradeMulti!: (payload: MultiDeletePayload) => Promise<string>;
|
||||
|
||||
currentPage = 1;
|
||||
|
||||
selectedItemIndex? = undefined;
|
||||
|
||||
filterText = '';
|
||||
|
||||
get rows(): number {
|
||||
return this.trades.length;
|
||||
}
|
||||
|
||||
perPage = this.activeTrades ? 200 : 25;
|
||||
|
||||
get filteredTrades() {
|
||||
return this.trades.slice(
|
||||
(this.currentPage - 1) * this.perPage,
|
||||
this.currentPage * this.perPage,
|
||||
);
|
||||
}
|
||||
|
||||
formatPriceWithDecimals(price) {
|
||||
return formatPrice(price, this.stakeCurrencyDecimals);
|
||||
}
|
||||
|
||||
forcesellHandler(item: Trade, ordertype: string | undefined = undefined) {
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(`Really forcesell trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiForcesellPayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
if (ordertype) {
|
||||
payload.ordertype = ordertype;
|
||||
const filteredTrades = computed(() => {
|
||||
return props.trades.slice((currentPage.value - 1) * perPage, currentPage.value * perPage);
|
||||
});
|
||||
const formatPriceWithDecimals = (price) => {
|
||||
return formatPrice(price, props.stakeCurrencyDecimals);
|
||||
};
|
||||
const forcesellHandler = (item: Trade, ordertype: string | undefined = undefined) => {
|
||||
root.$bvModal
|
||||
.msgBoxConfirm(`Really forcesell trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiForcesellPayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
if (ordertype) {
|
||||
payload.ordertype = ordertype;
|
||||
}
|
||||
botStore
|
||||
.forceSellMulti(payload)
|
||||
.then((xxx) => console.log(xxx))
|
||||
.catch((error) => console.log(error.response));
|
||||
}
|
||||
this.forceSellMulti(payload)
|
||||
.then((xxx) => console.log(xxx))
|
||||
.catch((error) => console.log(error.response));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleContextMenuEvent(item, index, event) {
|
||||
// stop browser context menu from appearing
|
||||
if (!this.activeTrades) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// log the selected item to the console
|
||||
console.log(item);
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
removeTradeHandler(item) {
|
||||
console.log(item);
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(`Really delete trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiDeletePayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
this.deleteTradeMulti(payload).catch((error) => console.log(error.response));
|
||||
}
|
||||
});
|
||||
}
|
||||
const removeTradeHandler = (item) => {
|
||||
console.log(item);
|
||||
root.$bvModal
|
||||
.msgBoxConfirm(`Really delete trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiDeletePayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
botStore.deleteTradeMulti(payload).catch((error) => console.log(error.response));
|
||||
}
|
||||
});
|
||||
};
|
||||
const tradeClick = (trade) => {
|
||||
botStore.activeBot.setDetailTrade(trade);
|
||||
};
|
||||
|
||||
tradeClick(trade) {
|
||||
this.setDetailTrade(trade);
|
||||
}
|
||||
}
|
||||
return {
|
||||
currentPage,
|
||||
filterText,
|
||||
perPage,
|
||||
filteredTrades,
|
||||
formatPriceWithDecimals,
|
||||
forcesellHandler,
|
||||
handleContextMenuEvent,
|
||||
removeTradeHandler,
|
||||
tradeClick,
|
||||
botStore,
|
||||
rows,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,50 +2,56 @@
|
|||
<div>
|
||||
<div class="mb-2">
|
||||
<label class="mr-auto h3">Daily Stats</label>
|
||||
<b-button class="float-right" size="sm" @click="getDaily">↻</b-button>
|
||||
<b-button class="float-right" size="sm" @click="botStore.activeBot.getDaily"
|
||||
>↻</b-button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<DailyChart v-if="dailyStats.data" :daily-stats="dailyStats" />
|
||||
<DailyChart
|
||||
v-if="botStore.activeBot.dailyStats.data"
|
||||
:daily-stats="botStore.activeBot.dailyStats"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<b-table class="table-sm" :items="dailyStats.data" :fields="dailyFields"> </b-table>
|
||||
<b-table class="table-sm" :items="botStore.activeBot.dailyStats.data" :fields="dailyFields">
|
||||
</b-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { defineComponent, computed, onMounted } from '@vue/composition-api';
|
||||
import DailyChart from '@/components/charts/DailyChart.vue';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
name: 'DailyStats',
|
||||
components: {
|
||||
DailyChart,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(StoreModules.ftbot, [BotStoreGetters.dailyStats]),
|
||||
dailyFields() {
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const dailyFields = computed(() => {
|
||||
return [
|
||||
{ key: 'date', label: 'Day' },
|
||||
{ key: 'abs_profit', label: 'Profit', formatter: (value) => formatPrice(value) },
|
||||
{
|
||||
key: 'fiat_value',
|
||||
label: `In ${this.dailyStats.fiat_display_currency}`,
|
||||
label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`,
|
||||
formatter: (value) => formatPrice(value, 2),
|
||||
},
|
||||
{ key: 'trade_count', label: 'Trades' },
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getDaily();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(StoreModules.ftbot, ['getDaily']),
|
||||
});
|
||||
onMounted(() => {
|
||||
botStore.activeBot.getDaily();
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
dailyFields,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
<div>
|
||||
<h3>Whitelist Methods</h3>
|
||||
|
||||
<div v-if="pairlistMethods.length" class="list">
|
||||
<b-list-group v-for="(method, key) in pairlistMethods" :key="key">
|
||||
<div v-if="botStore.activeBot.pairlistMethods.length" class="list">
|
||||
<b-list-group v-for="(method, key) in botStore.activeBot.pairlistMethods" :key="key">
|
||||
<b-list-group-item href="#" class="pair white">{{ method }}</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Show Whitelist -->
|
||||
<h3>Whitelist</h3>
|
||||
<div v-if="whitelist.length" class="list">
|
||||
<b-list-group v-for="(pair, key) in whitelist" :key="key">
|
||||
<div v-if="botStore.activeBot.whitelist.length" class="list">
|
||||
<b-list-group v-for="(pair, key) in botStore.activeBot.whitelist" :key="key">
|
||||
<b-list-group-item class="pair white">{{ pair }}</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
|
@ -31,12 +31,12 @@
|
|||
<b-button
|
||||
id="blacklist-add-btn"
|
||||
class="mr-1"
|
||||
:class="botApiVersion >= 1.12 ? 'col-6' : ''"
|
||||
:class="botStore.activeBot.botApiVersion >= 1.12 ? 'col-6' : ''"
|
||||
size="sm"
|
||||
>+</b-button
|
||||
>
|
||||
>+
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="botApiVersion >= 1.12"
|
||||
v-if="botStore.activeBot.botApiVersion >= 1.12"
|
||||
size="sm"
|
||||
class="col-6"
|
||||
title="Select pairs to delete pairs from your blacklist."
|
||||
|
@ -68,14 +68,15 @@
|
|||
size="sm"
|
||||
type="submit"
|
||||
@click="addBlacklistPair"
|
||||
>Add</b-button
|
||||
>
|
||||
Add</b-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</b-popover>
|
||||
</div>
|
||||
<div v-if="blacklist.length" class="list">
|
||||
<b-list-group v-for="(pair, key) in blacklist" :key="key">
|
||||
<div v-if="botStore.activeBot.blacklist.length" class="list">
|
||||
<b-list-group v-for="(pair, key) in botStore.activeBot.blacklist" :key="key">
|
||||
<b-list-group-item
|
||||
class="pair black"
|
||||
:active="blacklistSelect.indexOf(key) > -1"
|
||||
|
@ -91,87 +92,75 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BlacklistPayload, BlacklistResponse } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, ref, onMounted } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
export default defineComponent({
|
||||
name: 'FTBotAPIPairList',
|
||||
components: { DeleteIcon },
|
||||
setup() {
|
||||
const newblacklistpair = ref('');
|
||||
const blackListShow = ref(false);
|
||||
const blacklistSelect = ref<number[]>([]);
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Component({ components: { DeleteIcon } })
|
||||
export default class FTBotAPIPairList extends Vue {
|
||||
newblacklistpair = '';
|
||||
const initBlacklist = () => {
|
||||
if (botStore.activeBot.whitelist.length === 0) {
|
||||
botStore.activeBot.getWhitelist();
|
||||
}
|
||||
if (botStore.activeBot.blacklist.length === 0) {
|
||||
botStore.activeBot.getBlacklist();
|
||||
}
|
||||
};
|
||||
|
||||
blackListShow = false;
|
||||
const addBlacklistPair = () => {
|
||||
if (newblacklistpair.value) {
|
||||
blackListShow.value = false;
|
||||
|
||||
blacklistSelect: number[] = [];
|
||||
botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] });
|
||||
newblacklistpair.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
@ftbot.Action getWhitelist;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@ftbot.Action getBlacklist;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action addBlacklist!: (payload: BlacklistPayload) => Promise<BlacklistResponse>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action deleteBlacklist!: (payload: string[]) => Promise<BlacklistResponse>;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.whitelist]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.blacklist]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.pairlistMethods]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botApiVersion]: number;
|
||||
|
||||
created() {
|
||||
this.initBlacklist();
|
||||
}
|
||||
|
||||
initBlacklist() {
|
||||
if (this.whitelist.length === 0) {
|
||||
this.getWhitelist();
|
||||
}
|
||||
if (this.blacklist.length === 0) {
|
||||
this.getBlacklist();
|
||||
}
|
||||
}
|
||||
|
||||
addBlacklistPair() {
|
||||
if (this.newblacklistpair) {
|
||||
this.blackListShow = false;
|
||||
|
||||
this.addBlacklist({ blacklist: [this.newblacklistpair] });
|
||||
this.newblacklistpair = '';
|
||||
}
|
||||
}
|
||||
|
||||
blacklistSelectClick(key) {
|
||||
console.log(key);
|
||||
const index = this.blacklistSelect.indexOf(key);
|
||||
if (index > -1) {
|
||||
this.blacklistSelect.splice(index, 1);
|
||||
} else {
|
||||
this.blacklistSelect.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
deletePairs() {
|
||||
if (this.blacklistSelect.length === 0) {
|
||||
console.log('nothing to delete');
|
||||
return;
|
||||
}
|
||||
// const pairlist = this.blacklistSelect;
|
||||
const pairlist = this.blacklist.filter(
|
||||
(value, index) => this.blacklistSelect.indexOf(index) > -1,
|
||||
);
|
||||
console.log('Deleting pairs: ', pairlist);
|
||||
this.deleteBlacklist(pairlist);
|
||||
this.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();
|
||||
});
|
||||
return {
|
||||
addBlacklistPair,
|
||||
deletePairs,
|
||||
initBlacklist,
|
||||
blacklistSelectClick,
|
||||
botStore,
|
||||
newblacklistpair,
|
||||
blackListShow,
|
||||
blacklistSelect,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -188,15 +177,18 @@ export default class FTBotAPIPairList extends Vue {
|
|||
position: absolute;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.list-group-item.active .check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pair {
|
||||
border: 1px solid #ccc;
|
||||
background: #41b883;
|
||||
|
@ -206,6 +198,7 @@ export default class FTBotAPIPairList extends Vue {
|
|||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: white;
|
||||
color: black;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
>
|
||||
<form ref="form" @submit.stop.prevent="handleSubmit">
|
||||
<b-form-group
|
||||
v-if="botApiVersion >= 2.13 && shortAllowed"
|
||||
v-if="botStore.activeBot.botApiVersion >= 2.13 && botStore.activeBot.shortAllowed"
|
||||
label="Order direction (Long or Short)"
|
||||
label-for="order-direction"
|
||||
invalid-feedback="Stake-amount must be empty or a positive number"
|
||||
|
@ -45,8 +45,8 @@
|
|||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
v-if="botApiVersion > 1.12"
|
||||
:label="`*Stake-amount in ${stakeCurrency} [optional]`"
|
||||
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"
|
||||
>
|
||||
|
@ -59,7 +59,7 @@
|
|||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
v-if="botApiVersion > 1.1"
|
||||
v-if="botStore.activeBot.botApiVersion > 1.1"
|
||||
label="*OrderType"
|
||||
label-for="ordertype-input"
|
||||
invalid-feedback="OrderType"
|
||||
|
@ -79,99 +79,86 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BotState, ForceEnterPayload, OrderSides } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { ForceEnterPayload, OrderSides } from '@/types';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent, ref, nextTick } from '@vue/composition-api';
|
||||
|
||||
@Component({})
|
||||
export default class ForceBuyForm extends Vue {
|
||||
pair = '';
|
||||
export default defineComponent({
|
||||
name: 'ForceBuyForm',
|
||||
setup(_, { root }) {
|
||||
const botStore = useBotStore();
|
||||
|
||||
price = null;
|
||||
const form = ref<HTMLFormElement>();
|
||||
const pair = ref('');
|
||||
const price = ref<number | null>(null);
|
||||
const stakeAmount = ref<number | null>(null);
|
||||
const ordertype = ref('');
|
||||
const orderSide = ref<OrderSides>(OrderSides.long);
|
||||
|
||||
stakeAmount = null;
|
||||
const checkFormValidity = () => {
|
||||
const valid = form.value?.checkValidity();
|
||||
|
||||
ordertype?: string = '';
|
||||
return valid;
|
||||
};
|
||||
|
||||
orderSide: OrderSides = OrderSides.long;
|
||||
const handleSubmit = () => {
|
||||
// Exit when the form isn't valid
|
||||
if (!checkFormValidity()) {
|
||||
return;
|
||||
}
|
||||
// call forcebuy
|
||||
const payload: ForceEnterPayload = { pair: pair.value };
|
||||
if (price.value) {
|
||||
payload.price = Number(price.value);
|
||||
}
|
||||
if (ordertype.value) {
|
||||
payload.ordertype = ordertype.value;
|
||||
}
|
||||
if (stakeAmount.value) {
|
||||
payload.stakeamount = stakeAmount.value;
|
||||
}
|
||||
if (botStore.activeBot.botApiVersion >= 2.13) {
|
||||
payload.side = orderSide.value;
|
||||
}
|
||||
botStore.activeBot.forcebuy(payload);
|
||||
nextTick(() => {
|
||||
root.$bvModal.hide('forcebuy-modal');
|
||||
});
|
||||
};
|
||||
const resetForm = () => {
|
||||
console.log('resetForm');
|
||||
pair.value = '';
|
||||
price.value = null;
|
||||
stakeAmount.value = null;
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
$refs!: {
|
||||
form: HTMLFormElement;
|
||||
};
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botState]?: BotState;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.shortAllowed]?: boolean;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botApiVersion]: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action forcebuy!: (payload: ForceEnterPayload) => Promise<string>;
|
||||
|
||||
created() {
|
||||
this.$bvModal.show('forcebuy-modal');
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
checkFormValidity() {
|
||||
const valid = this.$refs.form.checkValidity();
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
handleBuy(bvModalEvt) {
|
||||
// Prevent modal from closing
|
||||
bvModalEvt.preventDefault();
|
||||
// Trigger submit handler
|
||||
this.handleSubmit();
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
console.log('resetForm');
|
||||
this.pair = '';
|
||||
this.price = null;
|
||||
this.stakeAmount = null;
|
||||
if (this.botApiVersion > 1.1) {
|
||||
this.ordertype =
|
||||
this.botState?.order_types?.forcebuy ||
|
||||
this.botState?.order_types?.forceentry ||
|
||||
this.botState?.order_types?.buy ||
|
||||
this.botState?.order_types?.entry;
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
// Exit when the form isn't valid
|
||||
if (!this.checkFormValidity()) {
|
||||
return;
|
||||
}
|
||||
// call forcebuy
|
||||
const payload: ForceEnterPayload = { pair: this.pair };
|
||||
if (this.price) {
|
||||
payload.price = Number(this.price);
|
||||
}
|
||||
if (this.ordertype) {
|
||||
payload.ordertype = this.ordertype;
|
||||
}
|
||||
if (this.stakeAmount) {
|
||||
payload.stakeamount = this.stakeAmount;
|
||||
}
|
||||
if (this.botApiVersion >= 2.13) {
|
||||
payload.side = this.orderSide;
|
||||
}
|
||||
this.forcebuy(payload);
|
||||
this.$nextTick(() => {
|
||||
this.$bvModal.hide('forcebuy-modal');
|
||||
});
|
||||
}
|
||||
}
|
||||
const handleBuy = (bvModalEvt) => {
|
||||
// Prevent modal from closing
|
||||
bvModalEvt.preventDefault();
|
||||
// Trigger submit handler
|
||||
handleSubmit();
|
||||
};
|
||||
return {
|
||||
handleSubmit,
|
||||
botStore,
|
||||
form,
|
||||
handleBuy,
|
||||
resetForm,
|
||||
pair,
|
||||
price,
|
||||
stakeAmount,
|
||||
ordertype,
|
||||
orderSide,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,38 +1,36 @@
|
|||
<template>
|
||||
<div class="d-flex h-100 p-0 align-items-start">
|
||||
<textarea v-model="formattedLogs" class="h-100" readonly></textarea>
|
||||
<b-button size="sm" @click="getLogs">↻</b-button>
|
||||
<b-button size="sm" @click="botStore.activeBot.getLogs">↻</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { LogLine } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { defineComponent, onMounted, computed } from '@vue/composition-api';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
export default defineComponent({
|
||||
name: 'LogViewer',
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Component({})
|
||||
export default class LogViewer extends Vue {
|
||||
@ftbot.Getter [BotStoreGetters.lastLogs]!: LogLine[];
|
||||
onMounted(() => botStore.activeBot.getLogs());
|
||||
|
||||
@ftbot.Action 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;
|
||||
});
|
||||
|
||||
mounted() {
|
||||
this.getLogs();
|
||||
}
|
||||
|
||||
get formattedLogs() {
|
||||
let result = '';
|
||||
for (let i = 0, len = this.lastLogs.length; i < len; i += 1) {
|
||||
const log = this.lastLogs[i];
|
||||
result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
formattedLogs,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
<div>
|
||||
<div class="mb-2">
|
||||
<label class="mr-auto h3">Pair Locks</label>
|
||||
<b-button class="float-right" size="sm" @click="getLocks">↻</b-button>
|
||||
<b-button class="float-right" size="sm" @click="botStore.activeBot.getLocks"
|
||||
>↻</b-button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<b-table class="table-sm" :items="currentLocks" :fields="tableFields">
|
||||
<b-table class="table-sm" :items="botStore.activeBot.activeLocks" :fields="tableFields">
|
||||
<template #cell(actions)="row">
|
||||
<b-button
|
||||
class="btn-xs ml-1"
|
||||
|
@ -23,51 +25,43 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { timestampms } from '@/shared/formatters';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { Lock } from '@/types';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import { AlertActions } from '@/store/modules/alerts';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { showAlert } from '@/stores/alerts';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
const alerts = namespace(StoreModules.alerts);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'PairLockList',
|
||||
components: { DeleteIcon },
|
||||
})
|
||||
export default class PairLockList extends Vue {
|
||||
@ftbot.Action getLocks;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.currentLocks]!: Lock[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action deleteLock!: (lockid: string) => Promise<string>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@alerts.Action [AlertActions.addAlert];
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
get tableFields() {
|
||||
return [
|
||||
const tableFields = [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'lock_end_timestamp', label: 'Until', formatter: 'timestampms' },
|
||||
{ key: 'reason', label: 'Reason' },
|
||||
{ key: 'actions' },
|
||||
];
|
||||
}
|
||||
|
||||
removePairLock(item: Lock) {
|
||||
console.log(item);
|
||||
if (item.id !== undefined) {
|
||||
this.deleteLock(item.id);
|
||||
} else {
|
||||
this.addAlert({ message: 'This Freqtrade version does not support deleting locks.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
const removePairLock = (item: Lock) => {
|
||||
console.log(item);
|
||||
if (item.id !== undefined) {
|
||||
botStore.activeBot.deleteLock(item.id);
|
||||
} else {
|
||||
showAlert('This Freqtrade version does not support deleting locks.');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
timestampms,
|
||||
botStore,
|
||||
tableFields,
|
||||
removePairLock,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
:key="comb.pair"
|
||||
button
|
||||
class="d-flex justify-content-between align-items-center py-1"
|
||||
:active="comb.pair === selectedPair"
|
||||
:active="comb.pair === botStore.activeBot.selectedPair"
|
||||
:title="`${comb.pair} - ${comb.tradeCount} trades`"
|
||||
@click="setSelectedPair(comb.pair)"
|
||||
@click="botStore.activeBot.setSelectedPair(comb.pair)"
|
||||
>
|
||||
<div>
|
||||
{{ comb.pair }}
|
||||
|
@ -20,7 +20,7 @@
|
|||
<ProfitPill
|
||||
v-if="backtestMode && comb.tradeCount > 0"
|
||||
:profit-ratio="comb.profit"
|
||||
:stake-currency="stakeCurrency"
|
||||
:stake-currency="botStore.activeBot.stakeCurrency"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
@ -28,15 +28,11 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { formatPercent, timestampms } from '@/shared/formatters';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { Lock, Trade } from '@/types';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
interface CombinedPairList {
|
||||
pair: string;
|
||||
|
@ -49,108 +45,102 @@ interface CombinedPairList {
|
|||
tradeCount: number;
|
||||
}
|
||||
|
||||
@Component({ components: { TradeProfit, ProfitPill } })
|
||||
export default class PairSummary extends Vue {
|
||||
@Prop({ required: true }) pairlist!: string[];
|
||||
export default defineComponent({
|
||||
name: 'PairSummary',
|
||||
components: { TradeProfit, ProfitPill },
|
||||
props: {
|
||||
// TOOD: Should be string list
|
||||
pairlist: { required: true, type: Array as () => string[] },
|
||||
currentLocks: { required: false, type: Array as () => Lock[], default: () => [] },
|
||||
trades: { required: true, type: Array as () => Trade[] },
|
||||
sortMethod: { default: 'normal', type: String },
|
||||
backtestMode: { required: false, default: false, type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const botStore = useBotStore();
|
||||
const combinedPairList = computed(() => {
|
||||
const comb: CombinedPairList[] = [];
|
||||
|
||||
@Prop({ required: false, default: () => [] }) currentLocks!: Lock[];
|
||||
props.pairlist.forEach((pair) => {
|
||||
const trades: Trade[] = props.trades.filter((el) => el.pair === pair);
|
||||
const allLocks = props.currentLocks.filter((el) => el.pair === pair);
|
||||
let lockReason = '';
|
||||
let locks;
|
||||
|
||||
@Prop({ required: true }) trades!: Trade[];
|
||||
|
||||
/** Sort method, "normal" (sorts by open trades > pairlist -> locks) or "profit" */
|
||||
@Prop({ required: false, default: 'normal' }) sortMethod!: string;
|
||||
|
||||
@Prop({ required: false, default: false }) backtestMode!: boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action setSelectedPair!: (pair: string) => void;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.selectedPair]!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
get combinedPairList() {
|
||||
const comb: CombinedPairList[] = [];
|
||||
|
||||
this.pairlist.forEach((pair) => {
|
||||
const trades: Trade[] = this.trades.filter((el) => el.pair === pair);
|
||||
const allLocks = this.currentLocks.filter((el) => el.pair === pair);
|
||||
let lockReason = '';
|
||||
let locks;
|
||||
|
||||
// Sort to have longer timeframe in front
|
||||
allLocks.sort((a, b) => (a.lock_end_timestamp > b.lock_end_timestamp ? -1 : 1));
|
||||
if (allLocks.length > 0) {
|
||||
[locks] = allLocks;
|
||||
lockReason = `${timestampms(locks.lock_end_timestamp)} - ${locks.reason}`;
|
||||
}
|
||||
let profitString = '';
|
||||
let profit = 0;
|
||||
let profitAbs = 0;
|
||||
trades.forEach((trade) => {
|
||||
profit += trade.profit_ratio;
|
||||
profitAbs += trade.profit_abs;
|
||||
// Sort to have longer timeframe in front
|
||||
allLocks.sort((a, b) => (a.lock_end_timestamp > b.lock_end_timestamp ? -1 : 1));
|
||||
if (allLocks.length > 0) {
|
||||
[locks] = allLocks;
|
||||
lockReason = `${timestampms(locks.lock_end_timestamp)} - ${locks.reason}`;
|
||||
}
|
||||
let profitString = '';
|
||||
let profit = 0;
|
||||
let profitAbs = 0;
|
||||
trades.forEach((trade) => {
|
||||
profit += trade.profit_ratio;
|
||||
profitAbs += trade.profit_abs;
|
||||
});
|
||||
const tradeCount = trades.length;
|
||||
const trade = tradeCount ? trades[0] : undefined;
|
||||
if (trades.length > 0) {
|
||||
profitString = `Current profit: ${formatPercent(profit)}`;
|
||||
}
|
||||
if (trade) {
|
||||
profitString += `\nOpen since: ${timestampms(trade.open_timestamp)}`;
|
||||
}
|
||||
comb.push({ pair, trade, locks, lockReason, profitString, profit, profitAbs, tradeCount });
|
||||
});
|
||||
const tradeCount = trades.length;
|
||||
const trade = tradeCount ? trades[0] : undefined;
|
||||
if (trades.length > 0) {
|
||||
profitString = `Current profit: ${formatPercent(profit)}`;
|
||||
if (props.sortMethod === 'profit') {
|
||||
comb.sort((a, b) => {
|
||||
if (a.profit > b.profit) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
} else {
|
||||
// sort Pairs: "with open trade" -> available -> locked
|
||||
comb.sort((a, b) => {
|
||||
if (a.trade && !b.trade) {
|
||||
return -1;
|
||||
}
|
||||
if (a.trade && b.trade) {
|
||||
// 2 open trade pairs
|
||||
return a.trade.trade_id > b.trade.trade_id ? 1 : -1;
|
||||
}
|
||||
if (!a.locks && b.locks) {
|
||||
return -1;
|
||||
}
|
||||
if (a.locks && b.locks) {
|
||||
// Both have locks
|
||||
return a.locks.lock_end_timestamp > b.locks.lock_end_timestamp ? 1 : -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
if (trade) {
|
||||
profitString += `\nOpen since: ${timestampms(trade.open_timestamp)}`;
|
||||
}
|
||||
comb.push({ pair, trade, locks, lockReason, profitString, profit, profitAbs, tradeCount });
|
||||
return comb;
|
||||
});
|
||||
if (this.sortMethod === 'profit') {
|
||||
comb.sort((a, b) => {
|
||||
if (a.profit > b.profit) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
} else {
|
||||
// sort Pairs: "with open trade" -> available -> locked
|
||||
comb.sort((a, b) => {
|
||||
if (a.trade && !b.trade) {
|
||||
return -1;
|
||||
}
|
||||
if (a.trade && b.trade) {
|
||||
// 2 open trade pairs
|
||||
return a.trade.trade_id > b.trade.trade_id ? 1 : -1;
|
||||
}
|
||||
if (!a.locks && b.locks) {
|
||||
return -1;
|
||||
}
|
||||
if (a.locks && b.locks) {
|
||||
// Both have locks
|
||||
return a.locks.lock_end_timestamp > b.locks.lock_end_timestamp ? 1 : -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
return comb;
|
||||
}
|
||||
|
||||
get tableFields() {
|
||||
return [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{
|
||||
key: 'locks.lock_end_timestamp',
|
||||
label: 'Lock',
|
||||
formatter: (value) => (value ? `${timestampms(value)}` : ''),
|
||||
},
|
||||
{
|
||||
key: 'trade.current_profit',
|
||||
label: 'Position',
|
||||
formatter: (value) => formatPercent(value, 3),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
const tableFields = computed(() => {
|
||||
return [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{
|
||||
key: 'locks.lock_end_timestamp',
|
||||
label: 'Lock',
|
||||
formatter: (value) => (value ? `${timestampms(value)}` : ''),
|
||||
},
|
||||
{
|
||||
key: 'trade.current_profit',
|
||||
label: 'Position',
|
||||
formatter: (value) => formatPercent(value, 3),
|
||||
},
|
||||
];
|
||||
});
|
||||
return {
|
||||
combinedPairList,
|
||||
tableFields,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -3,38 +3,39 @@
|
|||
<div class="mb-2">
|
||||
<h3>Performance</h3>
|
||||
</div>
|
||||
<b-table class="table-sm" :items="performanceStats" :fields="tableFields"></b-table>
|
||||
<b-table
|
||||
class="table-sm"
|
||||
:items="botStore.activeBot.performanceStats"
|
||||
:fields="tableFields"
|
||||
></b-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BotState, PerformanceEntry } from '@/types';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({})
|
||||
export default class Performance extends Vue {
|
||||
// TODO: Verify type of PerformanceStats!
|
||||
@ftbot.Getter [BotStoreGetters.performanceStats]!: PerformanceEntry[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botState]?: BotState;
|
||||
|
||||
get tableFields() {
|
||||
return [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'profit', label: 'Profit %' },
|
||||
{
|
||||
key: 'profit_abs',
|
||||
label: `Profit ${this.botState?.stake_currency}`,
|
||||
formatter: (v: number) => formatPrice(v, 5),
|
||||
},
|
||||
{ key: 'count', label: 'Count' },
|
||||
];
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'Performance',
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const tableFields = computed(() => {
|
||||
return [
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'profit', label: 'Profit %' },
|
||||
{
|
||||
key: 'profit_abs',
|
||||
label: `Profit ${botStore.activeBot.botState?.stake_currency}`,
|
||||
formatter: (v: number) => formatPrice(v, 5),
|
||||
},
|
||||
{ key: 'count', label: 'Count' },
|
||||
];
|
||||
});
|
||||
return {
|
||||
tableFields,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
title="Auto Refresh All bots"
|
||||
@click="allRefreshFull"
|
||||
@click="botStore.allRefreshFull"
|
||||
>
|
||||
<RefreshIcon :size="16" />
|
||||
</b-button>
|
||||
|
@ -18,35 +18,30 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import RefreshIcon from 'vue-material-design-icons/Refresh.vue';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
export default defineComponent({
|
||||
name: 'ReloadControl',
|
||||
components: { RefreshIcon },
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const autoRefreshLoc = computed({
|
||||
get() {
|
||||
return botStore.globalAutoRefresh;
|
||||
},
|
||||
set(newValue: boolean) {
|
||||
botStore.setGlobalAutoRefresh(newValue);
|
||||
},
|
||||
});
|
||||
|
||||
@Component({ components: { RefreshIcon } })
|
||||
export default class ReloadControl extends Vue {
|
||||
refreshInterval: number | null = null;
|
||||
|
||||
refreshIntervalSlow: number | null = null;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.globalAutoRefresh]!: boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action setGlobalAutoRefresh!: (newValue: boolean) => void;
|
||||
|
||||
@ftbot.Action allRefreshFull;
|
||||
|
||||
get autoRefreshLoc() {
|
||||
return this.globalAutoRefresh;
|
||||
}
|
||||
|
||||
set autoRefreshLoc(newValue: boolean) {
|
||||
this.setGlobalAutoRefresh(newValue);
|
||||
}
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
autoRefreshLoc,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -4,71 +4,59 @@
|
|||
<b-form-select
|
||||
id="strategy-select"
|
||||
v-model="locStrategy"
|
||||
:options="strategyList"
|
||||
@change="strategyChanged"
|
||||
:options="botStore.activeBot.strategyList"
|
||||
>
|
||||
</b-form-select>
|
||||
<div class="ml-2">
|
||||
<b-button @click="getStrategyList">↻</b-button>
|
||||
<b-button @click="botStore.activeBot.getStrategyList">↻</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea v-if="showDetails && strategy" v-model="strategyCode" class="w-100 h-100"></textarea>
|
||||
<textarea
|
||||
v-if="showDetails && botStore.activeBot.strategy"
|
||||
v-model="strategyCode"
|
||||
class="w-100 h-100"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { StrategyResult } from '@/types';
|
||||
import { Component, Vue, Prop, Emit } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { defineComponent, computed, onMounted } from '@vue/composition-api';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
export default defineComponent({
|
||||
name: 'StrategySelect',
|
||||
props: {
|
||||
value: { type: String, required: true },
|
||||
showDetails: { default: false, required: false, type: Boolean },
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@Component({})
|
||||
export default class StrategySelect extends Vue {
|
||||
@Prop() value!: string;
|
||||
const strategyCode = computed((): string => botStore.activeBot.strategy?.code);
|
||||
const locStrategy = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(strategy: string) {
|
||||
botStore.activeBot.getStrategy(strategy);
|
||||
emit('input', strategy);
|
||||
},
|
||||
});
|
||||
|
||||
@Prop({ default: false, required: false }) showDetails!: boolean;
|
||||
|
||||
@ftbot.Action getStrategyList;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action getStrategy!: (strategy: string) => void;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.strategyList]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.strategy]: StrategyResult;
|
||||
|
||||
@Emit('input')
|
||||
emitStrategy(strategy: string) {
|
||||
this.getStrategy(strategy);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
get strategyCode(): string {
|
||||
return this.strategy?.code;
|
||||
}
|
||||
|
||||
get locStrategy() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set locStrategy(val) {
|
||||
this.emitStrategy(val);
|
||||
}
|
||||
|
||||
strategyChanged(newVal) {
|
||||
this.value = newVal;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
if (this.strategyList.length === 0) {
|
||||
this.getStrategyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
if (botStore.activeBot.strategyList.length === 0) {
|
||||
botStore.activeBot.getStrategyList();
|
||||
}
|
||||
});
|
||||
return {
|
||||
botStore,
|
||||
strategyCode,
|
||||
locStrategy,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
@ -37,61 +37,64 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Emit, Prop, Watch } from 'vue-property-decorator';
|
||||
import { dateFromString, dateStringToTimeRange, timestampToDateString } from '@/shared/formatters';
|
||||
import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
|
||||
|
||||
const now = new Date();
|
||||
@Component({})
|
||||
export default class TimeRangeSelect extends Vue {
|
||||
dateFrom = '';
|
||||
|
||||
dateTo = '';
|
||||
export default defineComponent({
|
||||
name: 'TimeRangeSelect',
|
||||
props: {
|
||||
value: { required: true, type: String },
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const dateFrom = ref<string>('');
|
||||
const dateTo = ref<string>('');
|
||||
|
||||
@Prop() value!: string;
|
||||
const timeRange = computed(() => {
|
||||
if (dateFrom.value !== '' || dateTo.value !== '') {
|
||||
return `${dateStringToTimeRange(dateFrom.value)}-${dateStringToTimeRange(dateTo.value)}`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
@Emit('input')
|
||||
emitTimeRange() {
|
||||
return this.timeRange;
|
||||
}
|
||||
const updated = () => {
|
||||
emit('input', timeRange.value);
|
||||
};
|
||||
|
||||
@Watch('value')
|
||||
valueChanged(val) {
|
||||
console.log('TimeRange', val);
|
||||
if (val !== this.value) {
|
||||
this.updateInput();
|
||||
}
|
||||
}
|
||||
const updateInput = () => {
|
||||
const tr = props.value.split('-');
|
||||
if (tr[0]) {
|
||||
dateFrom.value = timestampToDateString(dateFromString(tr[0], 'yyyyMMdd'));
|
||||
}
|
||||
if (tr.length > 1 && tr[1]) {
|
||||
dateTo.value = timestampToDateString(dateFromString(tr[1], 'yyyyMMdd'));
|
||||
}
|
||||
updated();
|
||||
};
|
||||
|
||||
updateInput() {
|
||||
const tr = this.value.split('-');
|
||||
if (tr[0]) {
|
||||
this.dateFrom = timestampToDateString(dateFromString(tr[0], 'yyyyMMdd'));
|
||||
}
|
||||
if (tr.length > 1 && tr[1]) {
|
||||
this.dateTo = timestampToDateString(dateFromString(tr[1], 'yyyyMMdd'));
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => timeRange.value,
|
||||
() => updated(),
|
||||
);
|
||||
|
||||
created() {
|
||||
if (!this.value) {
|
||||
this.dateFrom = timestampToDateString(new Date(now.getFullYear(), now.getMonth() - 1, 1));
|
||||
} else {
|
||||
this.updateInput();
|
||||
}
|
||||
this.emitTimeRange();
|
||||
}
|
||||
onMounted(() => {
|
||||
if (!props.value) {
|
||||
dateFrom.value = timestampToDateString(new Date(now.getFullYear(), now.getMonth() - 1, 1));
|
||||
} else {
|
||||
updateInput();
|
||||
}
|
||||
emit('input', timeRange.value);
|
||||
});
|
||||
|
||||
updated() {
|
||||
this.emitTimeRange();
|
||||
}
|
||||
|
||||
get timeRange() {
|
||||
if (this.dateFrom !== '' || this.dateTo !== '') {
|
||||
return `${dateStringToTimeRange(this.dateFrom)}-${dateStringToTimeRange(this.dateTo)}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return {
|
||||
dateFrom,
|
||||
dateTo,
|
||||
timeRange,
|
||||
updated,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -8,59 +8,61 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
|
||||
@Component({})
|
||||
export default class Template extends Vue {
|
||||
selectedTimeframe = '';
|
||||
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',
|
||||
];
|
||||
|
||||
@Prop({ default: '' }) value!: string;
|
||||
const availableTimeframes = computed(() => {
|
||||
if (!props.belowTimeframe) {
|
||||
return availableTimeframesBase;
|
||||
}
|
||||
const idx = availableTimeframesBase.findIndex((v) => v === props.belowTimeframe);
|
||||
|
||||
// Filter available timeframes to be lower than this timeframe.
|
||||
@Prop({ default: '', required: false }) belowTimeframe!: string;
|
||||
return [...availableTimeframesBase].splice(0, idx);
|
||||
});
|
||||
|
||||
@Emit('input')
|
||||
emitSelectedTimeframe() {
|
||||
return this.selectedTimeframe;
|
||||
}
|
||||
const emitSelectedTimeframe = () => {
|
||||
emit('input', selectedTimeframe.value);
|
||||
};
|
||||
|
||||
@Watch('value')
|
||||
watchValue() {
|
||||
this.selectedTimeframe = this.value;
|
||||
}
|
||||
|
||||
get availableTimeframes() {
|
||||
if (!this.belowTimeframe) {
|
||||
return this.availableTimeframesBase;
|
||||
}
|
||||
const idx = this.availableTimeframesBase.findIndex((v) => v === this.belowTimeframe);
|
||||
|
||||
return [...this.availableTimeframesBase].splice(0, idx);
|
||||
}
|
||||
|
||||
// The below list must always remain sorted correctly!
|
||||
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',
|
||||
];
|
||||
}
|
||||
return {
|
||||
availableTimeframesBase,
|
||||
availableTimeframes,
|
||||
emitSelectedTimeframe,
|
||||
selectedTimeframe,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -99,29 +99,25 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TradeDetail',
|
||||
components: { ValuePair, TradeProfit, DateTimeTZ },
|
||||
})
|
||||
export default class TradeDetail extends Vue {
|
||||
@Prop({ type: Object, required: true }) trade!: Trade;
|
||||
|
||||
@Prop({ type: String, required: true }) stakeCurrency!: string;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
formatPriceCurrency = formatPriceCurrency;
|
||||
}
|
||||
props: {
|
||||
trade: { required: true, type: Object as () => Trade },
|
||||
stakeCurrency: { required: true, type: String },
|
||||
},
|
||||
setup() {
|
||||
return { timestampms, formatPercent, formatPrice, formatPriceCurrency };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.detail-header {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<b-popover :target="`btn-actions_${row.index}`" triggers="focus" placement="left">
|
||||
<trade-actions
|
||||
:trade="row.item"
|
||||
:bot-api-version="botApiVersion"
|
||||
:bot-api-version="botStore.activeBot.botApiVersion"
|
||||
@deleteTrade="removeTradeHandler"
|
||||
@forceSell="forcesellHandler"
|
||||
/>
|
||||
|
@ -44,7 +44,7 @@
|
|||
<template #cell(trade_id)="row">
|
||||
{{ row.item.trade_id }}
|
||||
{{
|
||||
botApiVersion > 2.0 && row.item.trading_mode !== 'spot'
|
||||
botStore.activeBot.botApiVersion > 2.0 && row.item.trading_mode !== 'spot'
|
||||
? '| ' + (row.item.is_short ? 'Short' : 'Long')
|
||||
: ''
|
||||
}}
|
||||
|
@ -80,197 +80,181 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||
import { MultiDeletePayload, MultiForcesellPayload, Trade } from '@/types';
|
||||
import ActionIcon from 'vue-material-design-icons/GestureTap.vue';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import TradeProfit from './TradeProfit.vue';
|
||||
import TradeActions from './TradeActions.vue';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
import { defineComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'TradeList',
|
||||
components: { ActionIcon, DateTimeTZ, TradeProfit, TradeActions },
|
||||
})
|
||||
export default class TradeList extends Vue {
|
||||
$refs!: {
|
||||
tradesTable: HTMLFormElement;
|
||||
};
|
||||
props: {
|
||||
trades: { required: true, type: Array as () => Array<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 },
|
||||
},
|
||||
setup(props, { root }) {
|
||||
const botStore = useBotStore();
|
||||
const currentPage = ref(1);
|
||||
const selectedItemIndex = ref();
|
||||
const filterText = ref('');
|
||||
const perPage = props.activeTrades ? 200 : 15;
|
||||
const tradesTable = ref<HTMLFormElement>();
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
@Prop({ required: true }) trades!: Array<Trade>;
|
||||
|
||||
@Prop({ default: 'Trades' }) title!: string;
|
||||
|
||||
@Prop({ required: false, default: '' }) stakeCurrency!: string;
|
||||
|
||||
@Prop({ default: false }) activeTrades!: boolean;
|
||||
|
||||
@Prop({ default: false }) showFilter!: boolean;
|
||||
|
||||
@Prop({ default: false, type: Boolean }) multiBotView!: boolean;
|
||||
|
||||
@Prop({ default: 'No Trades to show.' }) emptyText!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.detailTradeId]?: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrencyDecimals]!: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botApiVersion]: number;
|
||||
|
||||
@ftbot.Action setDetailTrade;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action forceSellMulti!: (payload: MultiForcesellPayload) => Promise<string>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action deleteTradeMulti!: (payload: MultiDeletePayload) => Promise<string>;
|
||||
|
||||
currentPage = 1;
|
||||
|
||||
selectedItemIndex? = undefined;
|
||||
|
||||
filterText = '';
|
||||
|
||||
@Watch('detailTradeId')
|
||||
watchTradeDetail(val) {
|
||||
const index = this.trades.findIndex((v) => v.trade_id === val);
|
||||
// Unselect when another tradeTable is selected!
|
||||
if (index < 0) {
|
||||
this.$refs.tradesTable.clearSelected();
|
||||
}
|
||||
}
|
||||
|
||||
get rows(): number {
|
||||
return this.trades.length;
|
||||
}
|
||||
|
||||
perPage = this.activeTrades ? 200 : 15;
|
||||
|
||||
// Added to table-fields for current trades
|
||||
openFields: Record<string, string | Function>[] = [{ key: 'actions' }];
|
||||
|
||||
// Added to table-fields for historic trades
|
||||
closedFields: Record<string, string | Function>[] = [
|
||||
{ key: 'close_timestamp', label: 'Close date' },
|
||||
{ key: 'exit_reason', label: 'Close Reason' },
|
||||
];
|
||||
|
||||
tableFields: Record<string, string | Function>[] = [
|
||||
this.multiBotView ? { key: 'botName', label: 'Bot' } : {},
|
||||
{ key: 'trade_id', label: 'ID' },
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'amount', label: 'Amount' },
|
||||
{
|
||||
key: 'stake_amount',
|
||||
label: 'Stake amount',
|
||||
formatter: (value: number) => this.formatPriceWithDecimals(value),
|
||||
},
|
||||
{
|
||||
key: 'open_rate',
|
||||
label: 'Open rate',
|
||||
formatter: (value: number) => this.formatPrice(value),
|
||||
},
|
||||
{
|
||||
key: this.activeTrades ? 'current_rate' : 'close_rate',
|
||||
label: this.activeTrades ? 'Current rate' : 'Close rate',
|
||||
formatter: (value: number) => this.formatPrice(value),
|
||||
},
|
||||
{
|
||||
key: 'profit',
|
||||
label: this.activeTrades ? 'Current profit %' : 'Profit %',
|
||||
formatter: (value: number, key, item: Trade) => {
|
||||
const percent = formatPercent(item.profit_ratio, 2);
|
||||
return `${percent} ${`(${this.formatPriceWithDecimals(item.profit_abs)})`}`;
|
||||
const openFields: Record<string, string | Function>[] = [{ key: 'actions' }];
|
||||
const closedFields: Record<string, string | Function>[] = [
|
||||
{ key: 'close_timestamp', label: 'Close date' },
|
||||
{ key: 'exit_reason', label: 'Close Reason' },
|
||||
];
|
||||
const formatPriceWithDecimals = (price) => {
|
||||
return formatPrice(price, botStore.activeBot.stakeCurrencyDecimals);
|
||||
};
|
||||
const rows = computed(() => {
|
||||
return props.trades.length;
|
||||
});
|
||||
const tableFields: Record<string, string | Function>[] = [
|
||||
props.multiBotView ? { key: 'botName', label: 'Bot' } : {},
|
||||
{ key: 'trade_id', label: 'ID' },
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'amount', label: 'Amount' },
|
||||
{
|
||||
key: 'stake_amount',
|
||||
label: 'Stake amount',
|
||||
formatter: (value: number) => formatPriceWithDecimals(value),
|
||||
},
|
||||
},
|
||||
{ key: 'open_timestamp', label: 'Open date' },
|
||||
...(this.activeTrades ? this.openFields : this.closedFields),
|
||||
];
|
||||
{
|
||||
key: 'open_rate',
|
||||
label: 'Open rate',
|
||||
formatter: (value: number) => formatPrice(value),
|
||||
},
|
||||
{
|
||||
key: props.activeTrades ? 'current_rate' : 'close_rate',
|
||||
label: props.activeTrades ? 'Current rate' : 'Close rate',
|
||||
formatter: (value: number) => formatPrice(value),
|
||||
},
|
||||
{
|
||||
key: 'profit',
|
||||
label: props.activeTrades ? 'Current profit %' : 'Profit %',
|
||||
formatter: (value: number, key, item: Trade) => {
|
||||
const percent = formatPercent(item.profit_ratio, 2);
|
||||
return `${percent} ${`(${formatPriceWithDecimals(item.profit_abs)})`}`;
|
||||
},
|
||||
},
|
||||
{ key: 'open_timestamp', label: 'Open date' },
|
||||
...(props.activeTrades ? openFields : closedFields),
|
||||
];
|
||||
|
||||
formatPriceWithDecimals(price) {
|
||||
return formatPrice(price, this.stakeCurrencyDecimals);
|
||||
}
|
||||
|
||||
forcesellHandler(item: Trade, ordertype: string | undefined = undefined) {
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(`Really forcesell trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiForcesellPayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
if (ordertype) {
|
||||
payload.ordertype = ordertype;
|
||||
const forcesellHandler = (item: Trade, ordertype: string | undefined = undefined) => {
|
||||
root.$bvModal
|
||||
.msgBoxConfirm(`Really forcesell trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiForcesellPayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
if (ordertype) {
|
||||
payload.ordertype = ordertype;
|
||||
}
|
||||
botStore
|
||||
.forceSellMulti(payload)
|
||||
.then((xxx) => console.log(xxx))
|
||||
.catch((error) => console.log(error.response));
|
||||
}
|
||||
this.forceSellMulti(payload)
|
||||
.then((xxx) => console.log(xxx))
|
||||
.catch((error) => console.log(error.response));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleContextMenuEvent(item, index, event) {
|
||||
// stop browser context menu from appearing
|
||||
if (!this.activeTrades) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// log the selected item to the console
|
||||
console.log(item);
|
||||
}
|
||||
|
||||
removeTradeHandler(item) {
|
||||
console.log(item);
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(`Really delete trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiDeletePayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
this.deleteTradeMulti(payload).catch((error) => console.log(error.response));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onRowClicked(item, index) {
|
||||
// Only allow single selection mode!
|
||||
if (
|
||||
item &&
|
||||
item.trade_id !== this.detailTradeId &&
|
||||
!this.$refs.tradesTable.isRowSelected(index)
|
||||
) {
|
||||
this.setDetailTrade(item);
|
||||
} else {
|
||||
console.log('unsetting item');
|
||||
this.setDetailTrade(null);
|
||||
}
|
||||
}
|
||||
|
||||
onRowSelected() {
|
||||
if (this.detailTradeId) {
|
||||
const itemIndex = this.trades.findIndex((v) => v.trade_id === this.detailTradeId);
|
||||
if (itemIndex >= 0) {
|
||||
this.$refs.tradesTable.selectRow(itemIndex);
|
||||
} else {
|
||||
console.log(`Unsetting item for tradeid ${this.selectedItemIndex}`);
|
||||
this.selectedItemIndex = undefined;
|
||||
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 removeTradeHandler = (item) => {
|
||||
console.log(item);
|
||||
root.$bvModal
|
||||
.msgBoxConfirm(`Really delete trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
const payload: MultiDeletePayload = {
|
||||
tradeid: String(item.trade_id),
|
||||
botId: item.botId,
|
||||
};
|
||||
botStore.deleteTradeMulti(payload).catch((error) => console.log(error.response));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onRowClicked = (item, index) => {
|
||||
// Only allow single selection mode!
|
||||
if (
|
||||
item &&
|
||||
item.trade_id !== botStore.activeBot.detailTradeId &&
|
||||
!tradesTable.value?.isRowSelected(index)
|
||||
) {
|
||||
botStore.activeBot.setDetailTrade(item);
|
||||
} else {
|
||||
console.log('unsetting item');
|
||||
botStore.activeBot.setDetailTrade(null);
|
||||
}
|
||||
};
|
||||
|
||||
const onRowSelected = () => {
|
||||
if (botStore.activeBot.detailTradeId) {
|
||||
const itemIndex = props.trades.findIndex(
|
||||
(v) => v.trade_id === botStore.activeBot.detailTradeId,
|
||||
);
|
||||
if (itemIndex >= 0) {
|
||||
tradesTable.value?.selectRow(itemIndex);
|
||||
} else {
|
||||
console.log(`Unsetting item for tradeid ${selectedItemIndex.value}`);
|
||||
selectedItemIndex.value = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => botStore.activeBot.detailTradeId,
|
||||
(val: number) => {
|
||||
const index = props.trades.findIndex((v) => v.trade_id === val);
|
||||
// Unselect when another tradeTable is selected!
|
||||
if (index < 0) {
|
||||
tradesTable.value?.clearSelected();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
botStore,
|
||||
currentPage,
|
||||
selectedItemIndex,
|
||||
filterText,
|
||||
perPage,
|
||||
tableFields,
|
||||
rows,
|
||||
tradesTable,
|
||||
forcesellHandler,
|
||||
handleContextMenuEvent,
|
||||
removeTradeHandler,
|
||||
onRowClicked,
|
||||
onRowSelected,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -10,25 +10,27 @@
|
|||
<script lang="ts">
|
||||
import { formatPercent, timestampms } from '@/shared/formatters';
|
||||
import { Trade } from '@/types';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||
|
||||
@Component({ components: { ProfitPill } })
|
||||
export default class TradeProfit extends Vue {
|
||||
@Prop({ required: true, type: Object }) trade!: Trade;
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
get profitDesc(): string {
|
||||
let profit = `Current profit: ${formatPercent(this.trade.profit_ratio)} (${
|
||||
this.trade.profit_abs
|
||||
})`;
|
||||
profit += `\nOpen since: ${timestampms(this.trade.open_timestamp)}`;
|
||||
return profit;
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'TradeProfit',
|
||||
components: { ProfitPill },
|
||||
props: {
|
||||
trade: { required: true, type: Object as () => Trade },
|
||||
},
|
||||
setup(props) {
|
||||
const profitDesc = computed((): string => {
|
||||
let profit = `Current profit: ${formatPercent(props.trade.profit_ratio)} (${
|
||||
props.trade.profit_abs
|
||||
})`;
|
||||
profit += `\nOpen since: ${timestampms(props.trade.open_timestamp)}`;
|
||||
return profit;
|
||||
});
|
||||
return { profitDesc };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -4,38 +4,39 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component({})
|
||||
export default class DateTimeTZ extends Vue {
|
||||
@Prop({ required: true, type: Number }) date!: number;
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
|
||||
@Prop({ required: false, type: Boolean, default: false }) showTimezone!: boolean;
|
||||
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);
|
||||
});
|
||||
|
||||
@Prop({ required: false, type: Boolean, default: false }) dateOnly!: boolean;
|
||||
const timezoneTooltip = computed((): string => {
|
||||
const time1 = timestampmsWithTimezone(props.date);
|
||||
const timeUTC = timestampmsWithTimezone(props.date, 'UTC');
|
||||
if (time1 === timeUTC) {
|
||||
return timeUTC;
|
||||
}
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
get formattedDate(): string {
|
||||
if (this.dateOnly) {
|
||||
return timestampToDateString(this.date);
|
||||
}
|
||||
if (this.showTimezone) {
|
||||
return timestampmsWithTimezone(this.date);
|
||||
}
|
||||
return timestampms(this.date);
|
||||
}
|
||||
|
||||
get timezoneTooltip(): string {
|
||||
const time1 = timestampmsWithTimezone(this.date);
|
||||
const timeUTC = timestampmsWithTimezone(this.date, 'UTC');
|
||||
if (time1 === timeUTC) {
|
||||
return timeUTC;
|
||||
}
|
||||
|
||||
return `${time1}\n${timeUTC}`;
|
||||
}
|
||||
}
|
||||
return `${time1}\n${timeUTC}`;
|
||||
});
|
||||
return { formattedDate, timezoneTooltip };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -20,41 +20,38 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatPercent, formatPrice, timestampms } from '@/shared/formatters';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||
|
||||
import ProfitSymbol from '@/components/general/ProfitSymbol.vue';
|
||||
|
||||
@Component({ components: { ProfitSymbol } })
|
||||
export default class ProfitPill extends Vue {
|
||||
@Prop({ required: false, default: undefined, type: Number }) profitRatio?: number;
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
|
||||
@Prop({ required: false, default: undefined, type: Number }) profitAbs?: number;
|
||||
export default defineComponent({
|
||||
name: 'ProfitPill',
|
||||
components: { ProfitSymbol },
|
||||
props: {
|
||||
profitRatio: { required: false, default: undefined, type: Number },
|
||||
profitAbs: { required: false, default: undefined, type: Number },
|
||||
stakeCurrency: { required: true, type: String },
|
||||
profitDesc: { required: false, default: '', type: String },
|
||||
},
|
||||
setup(props) {
|
||||
const isProfitable = computed(() => {
|
||||
return (
|
||||
(props.profitRatio !== undefined && props.profitRatio > 0) ||
|
||||
(props.profitRatio === undefined && props.profitAbs !== undefined && props.profitAbs > 0)
|
||||
);
|
||||
});
|
||||
|
||||
@Prop({ required: true, type: String }) stakeCurrency!: string;
|
||||
|
||||
@Prop({ required: false, default: '', type: String }) profitDesc!: string;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
get isProfitable() {
|
||||
return (
|
||||
(this.profitRatio !== undefined && this.profitRatio > 0) ||
|
||||
(this.profitRatio === undefined && this.profitAbs !== undefined && this.profitAbs > 0)
|
||||
);
|
||||
}
|
||||
|
||||
get profitString(): string {
|
||||
if (this.profitRatio !== undefined && this.profitAbs !== undefined) {
|
||||
return `(${formatPrice(this.profitAbs, 3)})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
const profitString = computed((): string => {
|
||||
if (props.profitRatio !== undefined && props.profitAbs !== undefined) {
|
||||
return `(${formatPrice(props.profitAbs, 3)})`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
return { profitString, isProfitable, formatPercent };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -5,16 +5,22 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
|
||||
@Component({})
|
||||
export default class ProfitSymbol extends Vue {
|
||||
@Prop({ required: true, type: Number }) profit!: number;
|
||||
|
||||
get isProfitable() {
|
||||
return this.profit > 0;
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'ProfitSymbol',
|
||||
props: {
|
||||
profit: { type: Number, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
const isProfitable = computed(() => {
|
||||
return props.profit > 0;
|
||||
});
|
||||
return {
|
||||
isProfitable,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
name: 'ValuePair',
|
||||
props: {
|
||||
description: {
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import BotAlerts from '@/components/ftbot/BotAlerts.vue';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Body',
|
||||
components: { BotAlerts },
|
||||
})
|
||||
export default class Body extends Vue {}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.container-main {
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
@Component({})
|
||||
export default class DraggableContainer extends Vue {
|
||||
@Prop({ required: true, type: String }) header!: string;
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'DraggableContainer',
|
||||
props: {
|
||||
header: { required: true, type: String },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -12,15 +12,15 @@
|
|||
|
||||
<b-collapse id="nav-collapse" class="text-right text-md-center" is-nav>
|
||||
<b-navbar-nav>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/trade"
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/trade"
|
||||
>Trade</router-link
|
||||
>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard"
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/dashboard"
|
||||
>Dashboard</router-link
|
||||
>
|
||||
<router-link class="nav-link navbar-nav" to="/graph">Chart</router-link>
|
||||
<router-link class="nav-link navbar-nav" to="/logs">Logs</router-link>
|
||||
<router-link v-if="canRunBacktest" class="nav-link navbar-nav" to="/backtest"
|
||||
<router-link v-if="botStore.canRunBacktest" class="nav-link navbar-nav" to="/backtest"
|
||||
>Backtest</router-link
|
||||
>
|
||||
<BootswatchThemeSelect />
|
||||
|
@ -31,7 +31,7 @@
|
|||
<!-- TODO This should show outside of the dropdown in XS mode -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<b-dropdown
|
||||
v-if="botCount > 1"
|
||||
v-if="botStore.botCount > 1"
|
||||
size="sm"
|
||||
class="m-1"
|
||||
no-caret
|
||||
|
@ -40,7 +40,7 @@
|
|||
menu-class="my-0 py-0"
|
||||
>
|
||||
<template #button-content>
|
||||
<BotEntry :bot="selectedBotObj" :no-buttons="true" />
|
||||
<BotEntry :bot="botStore.selectedBotObj" :no-buttons="true" />
|
||||
</template>
|
||||
<BotList :small="true" />
|
||||
</b-dropdown>
|
||||
|
@ -48,24 +48,31 @@
|
|||
</div>
|
||||
<li class="d-none d-sm-block nav-item text-secondary mr-2">
|
||||
<b-nav-text class="verticalCenter small mr-2">
|
||||
{{ botName || 'No bot selected' }}
|
||||
{{
|
||||
(botStore.activeBotorUndefined && botStore.activeBotorUndefined.botName) ||
|
||||
'No bot selected'
|
||||
}}
|
||||
</b-nav-text>
|
||||
<b-nav-text class="verticalCenter">
|
||||
{{ isBotOnline ? 'Online' : 'Offline' }}
|
||||
{{
|
||||
botStore.activeBotorUndefined && botStore.activeBotorUndefined.isBotOnline
|
||||
? 'Online'
|
||||
: 'Offline'
|
||||
}}
|
||||
</b-nav-text>
|
||||
</li>
|
||||
<li v-if="hasBots" class="nav-item">
|
||||
<li v-if="botStore.hasBots" class="nav-item">
|
||||
<!-- Hide dropdown on xs, instead show below -->
|
||||
<b-nav-item-dropdown id="avatar-drop" right class="d-none d-sm-block">
|
||||
<template #button-content>
|
||||
<b-avatar size="2em" button>FT</b-avatar>
|
||||
</template>
|
||||
<b-dropdown-item>V: {{ getUiVersion }}</b-dropdown-item>
|
||||
<b-dropdown-item>V: {{ settingsStore.uiVersion }}</b-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/settings">Settings</router-link>
|
||||
<b-checkbox v-model="layoutLockedLocal" class="pl-5">Lock layout</b-checkbox>
|
||||
<b-checkbox v-model="layoutStore.layoutLocked" class="pl-5">Lock layout</b-checkbox>
|
||||
<b-dropdown-item @click="resetDynamicLayout">Reset Layout</b-dropdown-item>
|
||||
<router-link
|
||||
v-if="botCount === 1"
|
||||
v-if="botStore.botCount === 1"
|
||||
class="dropdown-item"
|
||||
to="/"
|
||||
@click.native="clickLogout()"
|
||||
|
@ -77,16 +84,23 @@
|
|||
<li class="nav-item text-secondary ml-2 d-sm-none d-flex justify-content-between">
|
||||
<div class="d-flex">
|
||||
<b-nav-text class="verticalCenter small mr-2">
|
||||
{{ botName || 'No bot selected' }}
|
||||
{{
|
||||
(botStore.activeBotorUndefined && botStore.activeBotorUndefined.botName) ||
|
||||
'No bot selected'
|
||||
}}
|
||||
</b-nav-text>
|
||||
<b-nav-text class="verticalCenter">
|
||||
{{ isBotOnline ? 'Online' : 'Offline' }}
|
||||
{{
|
||||
botStore.activeBotorUndefined && botStore.activeBotorUndefined.isBotOnline
|
||||
? 'Online'
|
||||
: 'Offline'
|
||||
}}
|
||||
</b-nav-text>
|
||||
</div>
|
||||
</li>
|
||||
<router-link class="nav-link navbar-nav" to="/settings">Settings</router-link>
|
||||
<router-link
|
||||
v-if="botCount === 1"
|
||||
v-if="botStore.botCount === 1"
|
||||
class="nav-link navbar-nav"
|
||||
to="/"
|
||||
@click.native="clickLogout()"
|
||||
|
@ -105,166 +119,126 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||
import LoginModal from '@/views/LoginModal.vue';
|
||||
import { Action, namespace, Getter } from 'vuex-class';
|
||||
import BootswatchThemeSelect from '@/components/BootswatchThemeSelect.vue';
|
||||
import { LayoutActions, LayoutGetters } from '@/store/modules/layout';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import Favico from 'favico.js';
|
||||
import { OpenTradeVizOptions, SettingsGetters } from '@/store/modules/settings';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import ReloadControl from '@/components/ftbot/ReloadControl.vue';
|
||||
import BotEntry from '@/components/BotEntry.vue';
|
||||
import BotList from '@/components/BotList.vue';
|
||||
import { BotDescriptor } from '@/types';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, ref, onBeforeUnmount, onMounted, watch } from '@vue/composition-api';
|
||||
import { useRoute } from 'vue2-helpers/vue-router';
|
||||
import { OpenTradeVizOptions, useSettingsStore } from '@/stores/settings';
|
||||
import { useLayoutStore } from '@/stores/layout';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
const layoutNs = namespace(StoreModules.layout);
|
||||
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'NavBar',
|
||||
components: { LoginModal, BootswatchThemeSelect, ReloadControl, BotEntry, BotList },
|
||||
})
|
||||
export default class NavBar extends Vue {
|
||||
pingInterval: number | null = null;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
botSelectOpen = false;
|
||||
const settingsStore = useSettingsStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
const route = useRoute();
|
||||
const favicon = ref<Favico | undefined>(undefined);
|
||||
const pingInterval = ref<number>();
|
||||
|
||||
@Action setLoggedIn;
|
||||
const clickLogout = () => {
|
||||
botStore.removeBot(botStore.selectedBot);
|
||||
// TODO: This should be per bot
|
||||
};
|
||||
|
||||
@Action loadUIVersion;
|
||||
const setOpenTradesAsPill = (tradeCount: number) => {
|
||||
if (!favicon.value) {
|
||||
favicon.value = new Favico({
|
||||
animation: 'none',
|
||||
// position: 'up',
|
||||
// fontStyle: 'normal',
|
||||
// bgColor: '#',
|
||||
// textColor: '#FFFFFF',
|
||||
});
|
||||
}
|
||||
if (tradeCount !== 0 && settingsStore.openTradesInTitle === 'showPill') {
|
||||
favicon.value.badge(tradeCount);
|
||||
} else {
|
||||
favicon.value.reset();
|
||||
console.log('reset');
|
||||
}
|
||||
};
|
||||
const resetDynamicLayout = (): void => {
|
||||
console.log(`resetLayout called for ${route?.fullPath}`);
|
||||
switch (route?.fullPath) {
|
||||
case '/trade':
|
||||
layoutStore.resetTradingLayout();
|
||||
break;
|
||||
case '/dashboard':
|
||||
layoutStore.resetDashboardLayout();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
const setTitle = () => {
|
||||
let title = 'freqUI';
|
||||
if (settingsStore.openTradesInTitle === OpenTradeVizOptions.asTitle) {
|
||||
title = `(${botStore.activeBotorUndefined?.openTradeCount}) ${title}`;
|
||||
}
|
||||
if (botStore.activeBotorUndefined?.botName) {
|
||||
title = `${title} - ${botStore.activeBotorUndefined?.botName}`;
|
||||
}
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
@Getter getUiVersion!: string;
|
||||
onBeforeUnmount(() => {
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval.value);
|
||||
}
|
||||
});
|
||||
|
||||
@ftbot.Action pingAll;
|
||||
onMounted(() => {
|
||||
settingsStore.loadUIVersion();
|
||||
pingInterval.value = window.setInterval(botStore.pingAll, 60000);
|
||||
botStore.allRefreshFull();
|
||||
});
|
||||
|
||||
@ftbot.Action allGetState;
|
||||
settingsStore.$subscribe((_, state) => {
|
||||
const needsUpdate = settingsStore.openTradesInTitle !== state.openTradesInTitle;
|
||||
if (needsUpdate) {
|
||||
setTitle();
|
||||
setOpenTradesAsPill(botStore.activeBotorUndefined?.openTradeCount || 0);
|
||||
}
|
||||
});
|
||||
|
||||
@ftbot.Action logout;
|
||||
watch(
|
||||
() => botStore.activeBotorUndefined?.botName,
|
||||
() => setTitle(),
|
||||
);
|
||||
watch(
|
||||
() => botStore.activeBotorUndefined?.openTradeCount,
|
||||
() => {
|
||||
console.log('openTradeCount changed');
|
||||
if (
|
||||
settingsStore.openTradesInTitle === OpenTradeVizOptions.showPill &&
|
||||
botStore.activeBotorUndefined?.openTradeCount
|
||||
) {
|
||||
setOpenTradesAsPill(botStore.activeBotorUndefined.openTradeCount);
|
||||
} else if (settingsStore.openTradesInTitle === OpenTradeVizOptions.asTitle) {
|
||||
setTitle();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.isBotOnline]!: boolean;
|
||||
return {
|
||||
favicon,
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.hasBots]: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.botCount]: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botName]: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.openTradeCount]: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.canRunBacktest]!: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.selectedBotObj]!: BotDescriptor;
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||
|
||||
@layoutNs.Action [LayoutActions.resetDashboardLayout];
|
||||
|
||||
@layoutNs.Action [LayoutActions.resetTradingLayout];
|
||||
|
||||
@layoutNs.Action [LayoutActions.setLayoutLocked];
|
||||
|
||||
@uiSettingsNs.Getter [SettingsGetters.openTradesInTitle]: string;
|
||||
|
||||
favicon: Favico | undefined = undefined;
|
||||
|
||||
mounted() {
|
||||
this.pingAll();
|
||||
this.loadUIVersion();
|
||||
this.pingInterval = window.setInterval(this.pingAll, 60000);
|
||||
|
||||
if (this.hasBots) {
|
||||
// Query botstate - this will enable / disable certain modes
|
||||
this.allGetState();
|
||||
}
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
clickLogout(): void {
|
||||
this.logout();
|
||||
// TODO: This should be per bot
|
||||
this.setLoggedIn(false);
|
||||
}
|
||||
|
||||
get layoutLockedLocal() {
|
||||
return this.getLayoutLocked;
|
||||
}
|
||||
|
||||
set layoutLockedLocal(value: boolean) {
|
||||
this.setLayoutLocked(value);
|
||||
}
|
||||
|
||||
setOpenTradesAsPill(tradeCount: number) {
|
||||
if (!this.favicon) {
|
||||
this.favicon = new Favico({
|
||||
animation: 'none',
|
||||
// position: 'up',
|
||||
// fontStyle: 'normal',
|
||||
// bgColor: '#',
|
||||
// textColor: '#FFFFFF',
|
||||
});
|
||||
}
|
||||
if (tradeCount !== 0 && this.openTradesInTitle === 'showPill') {
|
||||
this.favicon.badge(tradeCount);
|
||||
} else {
|
||||
this.favicon.reset();
|
||||
console.log('reset');
|
||||
}
|
||||
}
|
||||
|
||||
resetDynamicLayout(): void {
|
||||
const route = this.$router.currentRoute.path;
|
||||
console.log(`resetLayout called for ${route}`);
|
||||
switch (route) {
|
||||
case '/trade':
|
||||
this.resetTradingLayout();
|
||||
break;
|
||||
case '/dashboard':
|
||||
this.resetDashboardLayout();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
let title = 'freqUI';
|
||||
if (this.openTradesInTitle === OpenTradeVizOptions.asTitle) {
|
||||
title = `(${this.openTradeCount}) ${title}`;
|
||||
}
|
||||
if (this.botName) {
|
||||
title = `${title} - ${this.botName}`;
|
||||
}
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
@Watch(BotStoreGetters.botName)
|
||||
botnameChanged() {
|
||||
this.setTitle();
|
||||
}
|
||||
|
||||
@Watch(BotStoreGetters.openTradeCount)
|
||||
openTradeCountChanged() {
|
||||
console.log('openTradeCount changed');
|
||||
if (this.openTradesInTitle === OpenTradeVizOptions.showPill) {
|
||||
this.setOpenTradesAsPill(this.openTradeCount);
|
||||
} else if (this.openTradesInTitle === OpenTradeVizOptions.asTitle) {
|
||||
this.setTitle();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch(SettingsGetters.openTradesInTitle)
|
||||
openTradesSettingChanged() {
|
||||
this.setTitle();
|
||||
this.setOpenTradesAsPill(this.openTradeCount);
|
||||
}
|
||||
}
|
||||
clickLogout,
|
||||
resetDynamicLayout,
|
||||
setTitle,
|
||||
layoutStore,
|
||||
botStore,
|
||||
settingsStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -3,23 +3,23 @@
|
|||
<!-- Only visible on xs (phone) viewport! -->
|
||||
<hr class="my-0" />
|
||||
<div class="d-flex flex-align-center justify-content-center">
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/open_trades">
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/open_trades">
|
||||
<OpenTradesIcon />
|
||||
Trades
|
||||
</router-link>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/trade_history">
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/trade_history">
|
||||
<ClosedTradesIcon />
|
||||
History
|
||||
</router-link>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/pairlist">
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/pairlist">
|
||||
<PairListIcon />
|
||||
Pairlist
|
||||
</router-link>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/balance">
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/balance">
|
||||
<BalanceIcon />
|
||||
Balance
|
||||
</router-link>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard">
|
||||
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/dashboard">
|
||||
<DashboardIcon />
|
||||
Dashboard
|
||||
</router-link>
|
||||
|
@ -28,23 +28,24 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
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/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'NavFooter',
|
||||
components: { OpenTradesIcon, ClosedTradesIcon, BalanceIcon, PairListIcon, DashboardIcon },
|
||||
})
|
||||
export default class NavFooter extends Vue {
|
||||
@ftbot.Getter [BotStoreGetters.canRunBacktest]!: boolean;
|
||||
}
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
return {
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
14
src/main.ts
14
src/main.ts
|
@ -2,16 +2,20 @@ import Vue from 'vue';
|
|||
import './plugins/bootstrap-vue';
|
||||
import './plugins/composition_api';
|
||||
import App from './App.vue';
|
||||
import store from './store';
|
||||
import router from './router';
|
||||
import { initApi } from './shared/apiService';
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
initApi(store);
|
||||
Vue.use(PiniaVuePlugin);
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
new Vue({
|
||||
store,
|
||||
router,
|
||||
render: (h) => h(App),
|
||||
pinia,
|
||||
}).$mount('#app');
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter, { RouteConfig } from 'vue-router';
|
||||
import Home from '@/views/Home.vue';
|
||||
import Error404 from '@/views/Error404.vue';
|
||||
import store from '@/store';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
import { initBots, useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const routes: Array<RouteConfig> = [
|
||||
{
|
||||
|
@ -88,8 +83,10 @@ const router = new VueRouter({
|
|||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const hasBots = store.getters[`${StoreModules.ftbot}/${MultiBotStoreGetters.hasBots}`];
|
||||
if (!to.meta?.allowAnonymous && !hasBots) {
|
||||
// Init bots here...
|
||||
initBots();
|
||||
const botStore = useBotStore();
|
||||
if (!to.meta?.allowAnonymous && !botStore.hasBots) {
|
||||
// Forward to login if login is required
|
||||
next({
|
||||
path: '/login',
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import axios from 'axios';
|
||||
import { UserService } from './userService';
|
||||
|
||||
/**
|
||||
* Global store variable - keep a reference here to be able to emmit alerts
|
||||
*/
|
||||
let globalStore;
|
||||
|
||||
export function useApi(userService: UserService, botId: string) {
|
||||
const api = axios.create({
|
||||
baseURL: userService.getBaseUrl(),
|
||||
|
@ -60,7 +56,8 @@ export function useApi(userService: UserService, botId: string) {
|
|||
}
|
||||
if ((err.response && err.response.status === 500) || err.message === 'Network Error') {
|
||||
console.log('Bot not running...');
|
||||
globalStore.dispatch(`ftbot/${botId}/setIsBotOnline`, false);
|
||||
const botStore = useBotStore();
|
||||
botStore.botStores[botId]?.setIsBotOnline(false);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -73,12 +70,3 @@ export function useApi(userService: UserService, botId: string) {
|
|||
api,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize api so store is accessible.
|
||||
* @param store Vuex store
|
||||
*/
|
||||
export function initApi(store) {
|
||||
globalStore = store;
|
||||
//
|
||||
}
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { getCurrentTheme, getTheme, storeCurrentTheme } from '@/shared/themes';
|
||||
import axios from 'axios';
|
||||
import { UserService } from '@/shared/userService';
|
||||
import { UiVersion } from '@/types';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import createBotStore, { MultiBotStoreGetters } from './modules/botStoreWrapper';
|
||||
import alertsModule from './modules/alerts';
|
||||
import layoutModule from './modules/layout';
|
||||
import settingsModule from './modules/settings';
|
||||
|
||||
Vue.use(Vuex);
|
||||
const initCurrentTheme = getCurrentTheme();
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
[StoreModules.alerts]: alertsModule,
|
||||
[StoreModules.layout]: layoutModule,
|
||||
[StoreModules.uiSettings]: settingsModule,
|
||||
},
|
||||
state: {
|
||||
currentTheme: initCurrentTheme,
|
||||
uiVersion: 'dev',
|
||||
},
|
||||
getters: {
|
||||
isDarkTheme(state) {
|
||||
const theme = getTheme(state.currentTheme);
|
||||
if (theme) {
|
||||
return theme.dark;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getChartTheme(state, getters) {
|
||||
return getters.isDarkTheme ? 'dark' : 'light';
|
||||
},
|
||||
getUiVersion(state) {
|
||||
return state.uiVersion;
|
||||
},
|
||||
loggedIn(state, getters) {
|
||||
return getters[`${StoreModules.ftbot}/${MultiBotStoreGetters.hasBots}`];
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
mutateCurrentTheme(state, newTheme: string) {
|
||||
storeCurrentTheme(newTheme);
|
||||
state.currentTheme = newTheme;
|
||||
},
|
||||
setUIVersion(state, uiVersion: string) {
|
||||
state.uiVersion = uiVersion;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setCurrentTheme({ commit }, newTheme: string) {
|
||||
commit('mutateCurrentTheme', newTheme);
|
||||
},
|
||||
|
||||
setLoggedIn({ commit }, loggedin: boolean) {
|
||||
commit('setLoggedIn', loggedin);
|
||||
},
|
||||
async loadUIVersion({ commit }) {
|
||||
if (import.meta.env.PROD) {
|
||||
try {
|
||||
const result = await axios.get<UiVersion>('/ui_version');
|
||||
const { version } = result.data;
|
||||
|
||||
commit('setUIVersion', version);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
UserService.migrateLogin();
|
||||
|
||||
store.registerModule(StoreModules.ftbot, createBotStore(store));
|
||||
Object.entries(UserService.getAvailableBots()).forEach(([, v]) => {
|
||||
store.dispatch(`${StoreModules.ftbot}/addBot`, v);
|
||||
});
|
||||
store.dispatch(`${StoreModules.ftbot}/selectFirstBot`);
|
||||
store.dispatch(`${StoreModules.ftbot}/startRefresh`);
|
||||
export default store;
|
|
@ -1,39 +0,0 @@
|
|||
import { AlertType } from '@/types/alertTypes';
|
||||
|
||||
export enum AlertActions {
|
||||
addAlert = 'addAlert',
|
||||
removeAlert = 'removeAlert',
|
||||
}
|
||||
|
||||
export enum AlertMutations {
|
||||
addAlert = 'addAlert',
|
||||
removeAlert = 'removeAlert',
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
activeMessages: [],
|
||||
},
|
||||
mutations: {
|
||||
[AlertMutations.addAlert](state, message: AlertType) {
|
||||
console.log(`adding message '${message.message}' to message queue`);
|
||||
state.activeMessages.push(message);
|
||||
},
|
||||
[AlertMutations.removeAlert](state) {
|
||||
state.activeMessages.shift();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[AlertActions.addAlert]({ commit }, message: AlertType) {
|
||||
commit(AlertMutations.addAlert, message);
|
||||
},
|
||||
[AlertActions.removeAlert]({ commit }) {
|
||||
commit(AlertMutations.removeAlert);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function showAlert(dispatch, message: string, severity = '') {
|
||||
dispatch(`alerts/${AlertActions.addAlert}`, { message, severity }, { root: true });
|
||||
}
|
|
@ -1,397 +0,0 @@
|
|||
import {
|
||||
BotDescriptor,
|
||||
BotDescriptors,
|
||||
DailyPayload,
|
||||
DailyRecord,
|
||||
DailyReturnValue,
|
||||
MultiDeletePayload,
|
||||
MultiForcesellPayload,
|
||||
RenameBotPayload,
|
||||
Trade,
|
||||
} from '@/types';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import StoreModules from '../storeSubModules';
|
||||
import { BotStoreActions, BotStoreGetters, createBotSubStore } from './ftbot';
|
||||
|
||||
const AUTH_SELECTED_BOT = 'ftSelectedBot';
|
||||
|
||||
interface FTMultiBotState {
|
||||
selectedBot: string;
|
||||
availableBots: BotDescriptors;
|
||||
globalAutoRefresh: boolean;
|
||||
refreshing: boolean;
|
||||
refreshInterval: number | null;
|
||||
refreshIntervalSlow: number | null;
|
||||
}
|
||||
|
||||
export enum MultiBotStoreGetters {
|
||||
hasBots = 'hasBots',
|
||||
botCount = 'botCount',
|
||||
nextBotId = 'nextBotId',
|
||||
selectedBot = 'selectedBot',
|
||||
selectedBotObj = 'selectedBotObj',
|
||||
globalAutoRefresh = 'globalAutoRefresh',
|
||||
allAvailableBots = 'allAvailableBots',
|
||||
allAvailableBotsList = 'allAvailableBotsList',
|
||||
allTradesAllBots = 'allTradesAllBots',
|
||||
allOpenTradesAllBots = 'allOpenTradesAllBots',
|
||||
allDailyStatsAllBots = 'allDailyStatsAllBots',
|
||||
// Automatically created entries
|
||||
allIsBotOnline = 'allIsBotOnline',
|
||||
allAutoRefresh = 'allAutoRefresh',
|
||||
allProfit = 'allProfit',
|
||||
allOpenTrades = 'allOpenTrades',
|
||||
allOpenTradeCount = 'allOpenTradeCount',
|
||||
allClosedTrades = 'allClosedTrades',
|
||||
allBotState = 'allBotState',
|
||||
allBalance = 'allBalance',
|
||||
}
|
||||
|
||||
const createAllGetters = [
|
||||
'isBotOnline',
|
||||
'autoRefresh',
|
||||
'closedTrades',
|
||||
'profit',
|
||||
'openTrades',
|
||||
'openTradeCount',
|
||||
'closedTrades',
|
||||
'botState',
|
||||
'balance',
|
||||
];
|
||||
|
||||
export default function createBotStore(store) {
|
||||
const state: FTMultiBotState = {
|
||||
selectedBot: '',
|
||||
availableBots: {},
|
||||
globalAutoRefresh: true,
|
||||
refreshing: false,
|
||||
refreshInterval: null,
|
||||
refreshIntervalSlow: null,
|
||||
};
|
||||
|
||||
// All getters working on all bots should be prefixed with all.
|
||||
const getters = {
|
||||
[MultiBotStoreGetters.hasBots](state: FTMultiBotState): boolean {
|
||||
return Object.keys(state.availableBots).length > 0;
|
||||
},
|
||||
[MultiBotStoreGetters.botCount](state: FTMultiBotState): number {
|
||||
return Object.keys(state.availableBots).length;
|
||||
},
|
||||
[MultiBotStoreGetters.nextBotId](state: FTMultiBotState): string {
|
||||
let botCount = Object.keys(state.availableBots).length;
|
||||
|
||||
while (`ftbot.${botCount}` in state.availableBots) {
|
||||
botCount += 1;
|
||||
}
|
||||
return `ftbot.${botCount}`;
|
||||
},
|
||||
[MultiBotStoreGetters.selectedBot](state: FTMultiBotState): string {
|
||||
return state.selectedBot;
|
||||
},
|
||||
[MultiBotStoreGetters.selectedBotObj](state: FTMultiBotState): BotDescriptor {
|
||||
return state.availableBots[state.selectedBot];
|
||||
},
|
||||
[MultiBotStoreGetters.globalAutoRefresh](state: FTMultiBotState): boolean {
|
||||
return state.globalAutoRefresh;
|
||||
},
|
||||
[MultiBotStoreGetters.allAvailableBots](state: FTMultiBotState): BotDescriptors {
|
||||
return state.availableBots;
|
||||
},
|
||||
[MultiBotStoreGetters.allAvailableBotsList](state: FTMultiBotState): string[] {
|
||||
return Object.keys(state.availableBots);
|
||||
},
|
||||
[MultiBotStoreGetters.allTradesAllBots](state: FTMultiBotState, getters): Trade[] {
|
||||
let resp: Trade[] = [];
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
const trades = getters[`${botId}/${BotStoreGetters.trades}`].map((t) => ({ ...t, botId }));
|
||||
|
||||
resp = resp.concat(trades);
|
||||
});
|
||||
return resp;
|
||||
},
|
||||
[MultiBotStoreGetters.allOpenTradesAllBots](state: FTMultiBotState, getters): Trade[] {
|
||||
let resp: Trade[] = [];
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
const trades = getters[`${botId}/${BotStoreGetters.openTrades}`].map((t) => ({
|
||||
...t,
|
||||
}));
|
||||
|
||||
resp = resp.concat(trades);
|
||||
});
|
||||
return resp;
|
||||
},
|
||||
[MultiBotStoreGetters.allDailyStatsAllBots](state: FTMultiBotState, getters): DailyReturnValue {
|
||||
const resp: Record<string, DailyRecord> = {};
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
getters[`${botId}/${BotStoreGetters.dailyStats}`]?.data?.forEach((d) => {
|
||||
if (!resp[d.date]) {
|
||||
resp[d.date] = { ...d };
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].abs_profit += d.abs_profit;
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].fiat_value += d.fiat_value;
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].trade_count += d.trade_count;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dailyReturn: DailyReturnValue = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
stake_currency: 'USDT',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
fiat_display_currency: 'USD',
|
||||
data: Object.values(resp),
|
||||
};
|
||||
return dailyReturn;
|
||||
},
|
||||
};
|
||||
// Autocreate getters from botStores
|
||||
Object.keys(BotStoreGetters).forEach((e) => {
|
||||
getters[e] = (state, getters) => {
|
||||
return getters[`${state.selectedBot}/${e}`];
|
||||
};
|
||||
});
|
||||
|
||||
// Create selected getters
|
||||
createAllGetters.forEach((e: string) => {
|
||||
const getterName = `all${e.charAt(0).toUpperCase() + e.slice(1)}`;
|
||||
console.log('creating getter', e, getterName);
|
||||
getters[getterName] = (state, getters) => {
|
||||
const result = {};
|
||||
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
result[botId] = getters[`${botId}/${e}`];
|
||||
});
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
const mutations = {
|
||||
selectBot(state: FTMultiBotState, botId: string) {
|
||||
if (botId in state.availableBots) {
|
||||
state.selectedBot = botId;
|
||||
} else {
|
||||
console.warn(`Botid ${botId} not available, but selected.`);
|
||||
}
|
||||
},
|
||||
setGlobalAutoRefresh(state, value: boolean) {
|
||||
state.globalAutoRefresh = value;
|
||||
},
|
||||
setRefreshing(state, refreshing: boolean) {
|
||||
state.refreshing = refreshing;
|
||||
},
|
||||
addBot(state: FTMultiBotState, bot: BotDescriptor) {
|
||||
// When Vue gets initialized, only existing objects will be added with reactivity.
|
||||
// To add reactivity to new property, we need to mutate the already reactive object.
|
||||
state.availableBots = {
|
||||
...state.availableBots,
|
||||
[bot.botId]: bot,
|
||||
};
|
||||
},
|
||||
renameBot(state: FTMultiBotState, bot: RenameBotPayload) {
|
||||
state.availableBots[bot.botId].botName = bot.botName;
|
||||
},
|
||||
removeBot(state: FTMultiBotState, botId: string) {
|
||||
if (botId in state.availableBots) {
|
||||
delete state.availableBots[botId];
|
||||
}
|
||||
},
|
||||
setRefreshInterval(state: FTMultiBotState, interval: number | null) {
|
||||
state.refreshInterval = interval;
|
||||
},
|
||||
setRefreshIntervalSlow(state: FTMultiBotState, interval: number | null) {
|
||||
state.refreshIntervalSlow = interval;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
// Actions automatically filled below
|
||||
addBot({ dispatch, getters, commit }, bot: BotDescriptor) {
|
||||
if (Object.keys(getters.allAvailableBots).includes(bot.botId)) {
|
||||
// throw 'Bot already present';
|
||||
// TODO: handle error!
|
||||
console.log('Bot already present');
|
||||
return;
|
||||
}
|
||||
console.log('add bot', bot);
|
||||
store.registerModule(
|
||||
[StoreModules.ftbot, bot.botId],
|
||||
createBotSubStore(bot.botId, bot.botName),
|
||||
);
|
||||
dispatch(`${bot.botId}/botAdded`);
|
||||
commit('addBot', bot);
|
||||
},
|
||||
renameBot({ dispatch, getters, commit }, bot: RenameBotPayload) {
|
||||
if (!Object.keys(getters.allAvailableBots).includes(bot.botId)) {
|
||||
// TODO: handle error!
|
||||
console.error('Bot not found');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(`${bot.botId}/rename`, bot.botName).then(() => {
|
||||
commit('renameBot', bot);
|
||||
});
|
||||
},
|
||||
removeBot({ commit, getters, dispatch }, botId: string) {
|
||||
if (Object.keys(getters.allAvailableBots).includes(botId)) {
|
||||
dispatch(`${botId}/logout`);
|
||||
store.unregisterModule([StoreModules.ftbot, botId]);
|
||||
commit('removeBot', botId);
|
||||
} else {
|
||||
console.warn(`bot ${botId} not found! could not remove`);
|
||||
}
|
||||
},
|
||||
selectFirstBot({ commit, getters }) {
|
||||
if (getters.hasBots) {
|
||||
const selBotId = localStorage.getItem(AUTH_SELECTED_BOT);
|
||||
const firstBot = Object.keys(getters.allAvailableBots)[0];
|
||||
let selBot: string | undefined = firstBot;
|
||||
if (selBotId) {
|
||||
selBot = Object.keys(getters.allAvailableBots).find((x) => x === selBotId);
|
||||
}
|
||||
commit('selectBot', getters.allAvailableBots[selBot || firstBot].botId);
|
||||
}
|
||||
},
|
||||
selectBot({ commit }, botId: string) {
|
||||
localStorage.setItem(AUTH_SELECTED_BOT, botId);
|
||||
commit('selectBot', botId);
|
||||
},
|
||||
setGlobalAutoRefresh({ commit }, value: boolean) {
|
||||
commit('setGlobalAutoRefresh', value);
|
||||
},
|
||||
allRefreshFrequent({ dispatch, getters }, forceUpdate = false) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
if (
|
||||
getters[`${e}/${BotStoreGetters.refreshNow}`] &&
|
||||
(getters[MultiBotStoreGetters.globalAutoRefresh] || forceUpdate)
|
||||
) {
|
||||
// console.log('refreshing', e);
|
||||
dispatch(`${e}/${BotStoreActions.refreshFrequent}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
allRefreshSlow({ dispatch, getters }, forceUpdate = false) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
if (
|
||||
getters[`${e}/${BotStoreGetters.refreshNow}`] &&
|
||||
(getters[MultiBotStoreGetters.globalAutoRefresh] || forceUpdate)
|
||||
) {
|
||||
dispatch(`${e}/${BotStoreActions.refreshSlow}`, forceUpdate);
|
||||
}
|
||||
});
|
||||
},
|
||||
async allRefreshFull({ commit, dispatch, state, getters }) {
|
||||
if (state.refreshing) {
|
||||
return;
|
||||
}
|
||||
commit('setRefreshing', true);
|
||||
try {
|
||||
// Ensure all bots status is correct.
|
||||
await dispatch('pingAll');
|
||||
getters.allAvailableBotsList.forEach(async (e) => {
|
||||
if (
|
||||
getters[`${e}/${BotStoreGetters.isBotOnline}`] &&
|
||||
!getters[`${e}/${BotStoreGetters.botStatusAvailable}`]
|
||||
) {
|
||||
await dispatch('allGetState');
|
||||
}
|
||||
});
|
||||
const updates: Promise<AxiosInstance>[] = [];
|
||||
updates.push(dispatch('allRefreshFrequent', false));
|
||||
updates.push(dispatch('allRefreshSlow', true));
|
||||
// updates.push(dispatch('getDaily'));
|
||||
// updates.push(dispatch('getBalance'));
|
||||
|
||||
await Promise.all(updates);
|
||||
console.log('refreshing_end');
|
||||
} finally {
|
||||
commit('setRefreshing', false);
|
||||
}
|
||||
},
|
||||
|
||||
startRefresh({ state, dispatch, commit }) {
|
||||
console.log('Starting automatic refresh.');
|
||||
dispatch('allRefreshFull');
|
||||
|
||||
if (!state.refreshInterval) {
|
||||
// Set interval for refresh
|
||||
const refreshInterval = window.setInterval(() => {
|
||||
dispatch('allRefreshFrequent');
|
||||
}, 5000);
|
||||
commit('setRefreshInterval', refreshInterval);
|
||||
}
|
||||
if (!state.refreshIntervalSlow) {
|
||||
const refreshIntervalSlow = window.setInterval(() => {
|
||||
dispatch('allRefreshSlow', false);
|
||||
}, 60000);
|
||||
commit('setRefreshIntervalSlow', refreshIntervalSlow);
|
||||
}
|
||||
},
|
||||
stopRefresh({ state, commit }: { state: FTMultiBotState; commit: any }) {
|
||||
console.log('Stopping automatic refresh.');
|
||||
if (state.refreshInterval) {
|
||||
window.clearInterval(state.refreshInterval);
|
||||
commit('setRefreshInterval', null);
|
||||
}
|
||||
if (state.refreshIntervalSlow) {
|
||||
window.clearInterval(state.refreshIntervalSlow);
|
||||
commit('setRefreshIntervalSlow', null);
|
||||
}
|
||||
},
|
||||
|
||||
async pingAll({ getters, dispatch }) {
|
||||
await Promise.all(
|
||||
getters.allAvailableBotsList.map(async (e) => {
|
||||
try {
|
||||
await dispatch(`${e}/ping`);
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
allGetState({ getters, dispatch }) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
dispatch(`${e}/getState`);
|
||||
});
|
||||
},
|
||||
allGetDaily({ getters, dispatch }, payload: DailyPayload) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
dispatch(`${e}/getDaily`, payload);
|
||||
});
|
||||
},
|
||||
async forceSellMulti({ dispatch }, forcesellPayload: MultiForcesellPayload) {
|
||||
return dispatch(`${forcesellPayload.botId}/${[BotStoreActions.forcesell]}`, forcesellPayload);
|
||||
},
|
||||
async deleteTradeMulti({ dispatch }, deletePayload: MultiDeletePayload) {
|
||||
return dispatch(
|
||||
`${deletePayload.botId}/${[BotStoreActions.deleteTrade]}`,
|
||||
deletePayload.tradeid,
|
||||
);
|
||||
},
|
||||
};
|
||||
// Autocreate Actions from botstores
|
||||
Object.keys(BotStoreActions).forEach((e) => {
|
||||
actions[e] = ({ state, dispatch, getters }, ...args) => {
|
||||
if (getters.hasBots) {
|
||||
return dispatch(`${state.selectedBot}/${e}`, ...args);
|
||||
}
|
||||
console.warn(`bot ${state.selectedBot} is not registered.`);
|
||||
return {};
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
namespaced: true,
|
||||
// modules: {
|
||||
// 'ftbot.0': createBotSubStore('ftbot.0'),
|
||||
// },
|
||||
state,
|
||||
mutations,
|
||||
|
||||
getters,
|
||||
actions,
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,116 +0,0 @@
|
|||
import { getPlotConfigName, getAllPlotConfigNames } from '@/shared/storage';
|
||||
|
||||
import {
|
||||
BotState,
|
||||
Trade,
|
||||
PlotConfig,
|
||||
StrategyResult,
|
||||
BalanceInterface,
|
||||
DailyReturnValue,
|
||||
LockResponse,
|
||||
PlotConfigStorage,
|
||||
ProfitInterface,
|
||||
BacktestResult,
|
||||
StrategyBacktestResult,
|
||||
BacktestSteps,
|
||||
LogLine,
|
||||
SysInfoResponse,
|
||||
LoadingStatus,
|
||||
BacktestHistoryEntry,
|
||||
} from '@/types';
|
||||
|
||||
export interface FtbotStateType {
|
||||
ping: string;
|
||||
botStatusAvailable: boolean;
|
||||
isBotOnline: boolean;
|
||||
autoRefresh: boolean;
|
||||
refreshing: boolean;
|
||||
version: string;
|
||||
lastLogs: LogLine[];
|
||||
refreshRequired: boolean;
|
||||
trades: Trade[];
|
||||
openTrades: Trade[];
|
||||
tradeCount: number;
|
||||
performanceStats: Performance[];
|
||||
whitelist: string[];
|
||||
blacklist: string[];
|
||||
profit: ProfitInterface | {};
|
||||
botState?: BotState;
|
||||
balance: BalanceInterface | {};
|
||||
dailyStats: DailyReturnValue | {};
|
||||
pairlistMethods: string[];
|
||||
detailTradeId?: number;
|
||||
selectedPair: string;
|
||||
// TODO: type me
|
||||
candleData: {};
|
||||
candleDataStatus: LoadingStatus;
|
||||
// TODO: type me
|
||||
history: {};
|
||||
historyStatus: LoadingStatus;
|
||||
strategyPlotConfig?: PlotConfig;
|
||||
customPlotConfig: PlotConfigStorage;
|
||||
plotConfigName: string;
|
||||
availablePlotConfigNames: string[];
|
||||
strategyList: string[];
|
||||
strategy: StrategyResult | {};
|
||||
pairlist: string[];
|
||||
currentLocks?: LockResponse;
|
||||
backtestRunning: boolean;
|
||||
backtestProgress: number;
|
||||
backtestStep: BacktestSteps;
|
||||
backtestTradeCount: number;
|
||||
backtestResult?: BacktestResult;
|
||||
selectedBacktestResultKey: string;
|
||||
backtestHistory: Record<string, StrategyBacktestResult>;
|
||||
backtestHistoryList: BacktestHistoryEntry[];
|
||||
sysinfo: SysInfoResponse | {};
|
||||
}
|
||||
|
||||
const state = (): FtbotStateType => {
|
||||
return {
|
||||
ping: '',
|
||||
botStatusAvailable: false,
|
||||
isBotOnline: false,
|
||||
autoRefresh: false,
|
||||
refreshing: false,
|
||||
version: '',
|
||||
lastLogs: [],
|
||||
refreshRequired: true,
|
||||
trades: [],
|
||||
openTrades: [],
|
||||
tradeCount: 0,
|
||||
performanceStats: [],
|
||||
whitelist: [],
|
||||
blacklist: [],
|
||||
profit: {},
|
||||
botState: undefined,
|
||||
balance: {},
|
||||
dailyStats: {},
|
||||
pairlistMethods: [],
|
||||
detailTradeId: undefined,
|
||||
selectedPair: '',
|
||||
candleData: {},
|
||||
candleDataStatus: 'loading',
|
||||
history: {},
|
||||
historyStatus: 'loading',
|
||||
strategyPlotConfig: undefined,
|
||||
customPlotConfig: {},
|
||||
plotConfigName: getPlotConfigName(),
|
||||
availablePlotConfigNames: getAllPlotConfigNames(),
|
||||
strategyList: [],
|
||||
strategy: {},
|
||||
pairlist: [],
|
||||
currentLocks: undefined,
|
||||
// backtesting
|
||||
backtestRunning: false,
|
||||
backtestProgress: 0.0,
|
||||
backtestStep: BacktestSteps.none,
|
||||
backtestTradeCount: 0,
|
||||
backtestResult: undefined,
|
||||
selectedBacktestResultKey: '',
|
||||
backtestHistory: {},
|
||||
backtestHistoryList: [],
|
||||
sysinfo: {},
|
||||
};
|
||||
};
|
||||
export default state;
|
|
@ -1,170 +0,0 @@
|
|||
import { GridItemData } from 'vue-grid-layout';
|
||||
|
||||
export enum TradeLayout {
|
||||
multiPane = 'g-multiPane',
|
||||
openTrades = 'g-openTrades',
|
||||
tradeHistory = 'g-tradeHistory',
|
||||
tradeDetail = 'g-tradeDetail',
|
||||
chartView = 'g-chartView',
|
||||
}
|
||||
|
||||
export enum DashboardLayout {
|
||||
dailyChart = 'g-dailyChart',
|
||||
botComparison = 'g-botComparison',
|
||||
allOpenTrades = 'g-allOpenTrades',
|
||||
cumChartChart = 'g-cumChartChart',
|
||||
tradesLogChart = 'g-TradesLogChart',
|
||||
}
|
||||
|
||||
export enum LayoutGetters {
|
||||
getDashboardLayoutSm = 'getDashboardLayoutSm',
|
||||
getDashboardLayout = 'getDashboardLayout',
|
||||
getTradingLayoutSm = 'getTradingLayoutSm',
|
||||
getTradingLayout = 'getTradingLayout',
|
||||
getLayoutLocked = 'getLayoutLocked',
|
||||
}
|
||||
|
||||
export enum LayoutActions {
|
||||
setDashboardLayout = 'setDashboardLayout',
|
||||
setTradingLayout = 'setTradingLayout',
|
||||
resetDashboardLayout = 'resetDashboardLayout',
|
||||
resetTradingLayout = 'resetTradingLayout',
|
||||
setLayoutLocked = 'setLayoutLocked',
|
||||
}
|
||||
|
||||
export enum LayoutMutations {
|
||||
setDashboardLayout = 'setDashboardLayout',
|
||||
setTradingLayout = 'setTradingLayout',
|
||||
setLayoutLocked = 'setLayoutLocked',
|
||||
}
|
||||
// Define default layouts
|
||||
const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 3, h: 35 },
|
||||
{ i: TradeLayout.chartView, x: 3, y: 0, w: 9, h: 14 },
|
||||
{ i: TradeLayout.tradeDetail, x: 3, y: 19, w: 9, h: 6 },
|
||||
{ i: TradeLayout.openTrades, x: 3, y: 14, w: 9, h: 5 },
|
||||
{ i: TradeLayout.tradeHistory, x: 3, y: 25, w: 9, h: 10 },
|
||||
];
|
||||
|
||||
// Currently only multiPane is visible
|
||||
const DEFAULT_TRADING_LAYOUT_SM: GridItemData[] = [
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 12, h: 10 },
|
||||
{ i: TradeLayout.chartView, x: 0, y: 10, w: 12, h: 0 },
|
||||
{ i: TradeLayout.tradeDetail, x: 0, y: 19, w: 12, h: 0 },
|
||||
{ i: TradeLayout.openTrades, x: 0, y: 8, w: 12, h: 0 },
|
||||
{ i: TradeLayout.tradeHistory, x: 0, y: 25, w: 12, h: 0 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 8, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.dailyChart, x: 8, y: 0, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 8, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 8, y: 6, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 12, w: 12, h: 4 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT_SM: GridItemData[] = [
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 12, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 12, h: 8 },
|
||||
{ i: DashboardLayout.dailyChart, x: 0, y: 14, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 0, y: 20, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 26, w: 12, h: 4 },
|
||||
];
|
||||
|
||||
const STORE_DASHBOARD_LAYOUT = 'ftDashboardLayout';
|
||||
const STORE_TRADING_LAYOUT = 'ftTradingLayout';
|
||||
const STORE_LAYOUT_LOCK = 'ftLayoutLocked';
|
||||
|
||||
function getLayoutLocked() {
|
||||
const fromStore = localStorage.getItem(STORE_LAYOUT_LOCK);
|
||||
if (fromStore) {
|
||||
return JSON.parse(fromStore);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLayout(storageString: string, defaultLayout: GridItemData[]) {
|
||||
const fromStore = localStorage.getItem(storageString);
|
||||
if (fromStore) {
|
||||
return JSON.parse(fromStore);
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(defaultLayout));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function finding a layout entry
|
||||
* @param gridLayout Array of grid layouts used in this layout. Must be passed to GridLayout, too.
|
||||
* @param name Name within the dashboard layout to find
|
||||
*/
|
||||
export function findGridLayout(gridLayout: GridItemData[], name: string): GridItemData {
|
||||
let layout = gridLayout.find((value) => value.i === name);
|
||||
if (!layout) {
|
||||
layout = { i: name, x: 0, y: 0, w: 4, h: 6 };
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
dashboardLayout: getLayout(STORE_DASHBOARD_LAYOUT, DEFAULT_DASHBOARD_LAYOUT),
|
||||
tradingLayout: getLayout(STORE_TRADING_LAYOUT, DEFAULT_TRADING_LAYOUT),
|
||||
layoutLocked: getLayoutLocked(),
|
||||
},
|
||||
|
||||
getters: {
|
||||
[LayoutGetters.getDashboardLayoutSm]() {
|
||||
return [...DEFAULT_DASHBOARD_LAYOUT_SM];
|
||||
},
|
||||
[LayoutGetters.getDashboardLayout](state) {
|
||||
return state.dashboardLayout;
|
||||
},
|
||||
[LayoutGetters.getTradingLayoutSm]() {
|
||||
return [...DEFAULT_TRADING_LAYOUT_SM];
|
||||
},
|
||||
[LayoutGetters.getTradingLayout](state) {
|
||||
return state.tradingLayout;
|
||||
},
|
||||
[LayoutGetters.getLayoutLocked](state) {
|
||||
return state.layoutLocked;
|
||||
},
|
||||
},
|
||||
|
||||
mutations: {
|
||||
[LayoutMutations.setDashboardLayout](state, layout) {
|
||||
state.dashboardLayout = layout;
|
||||
localStorage.setItem(STORE_DASHBOARD_LAYOUT, JSON.stringify(layout));
|
||||
},
|
||||
[LayoutMutations.setTradingLayout](state, layout) {
|
||||
state.tradingLayout = layout;
|
||||
localStorage.setItem(STORE_TRADING_LAYOUT, JSON.stringify(layout));
|
||||
},
|
||||
[LayoutMutations.setLayoutLocked](state, locked: boolean) {
|
||||
state.layoutLocked = locked;
|
||||
localStorage.setItem(STORE_LAYOUT_LOCK, JSON.stringify(locked));
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
[LayoutActions.setDashboardLayout]({ commit }, layout) {
|
||||
commit(LayoutMutations.setDashboardLayout, layout);
|
||||
},
|
||||
[LayoutActions.setTradingLayout]({ commit }, layout) {
|
||||
commit(LayoutMutations.setTradingLayout, layout);
|
||||
},
|
||||
[LayoutActions.setLayoutLocked]({ commit }, locked: boolean) {
|
||||
commit(LayoutMutations.setLayoutLocked, locked);
|
||||
},
|
||||
[LayoutActions.resetDashboardLayout]({ commit }) {
|
||||
commit(
|
||||
LayoutMutations.setDashboardLayout,
|
||||
JSON.parse(JSON.stringify(DEFAULT_DASHBOARD_LAYOUT)),
|
||||
);
|
||||
},
|
||||
|
||||
[LayoutActions.resetTradingLayout]({ commit }) {
|
||||
commit(LayoutMutations.setTradingLayout, JSON.parse(JSON.stringify(DEFAULT_TRADING_LAYOUT)));
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,96 +0,0 @@
|
|||
import { setTimezone } from '@/shared/formatters';
|
||||
|
||||
const STORE_UI_SETTINGS = 'ftUISettings';
|
||||
|
||||
export enum OpenTradeVizOptions {
|
||||
showPill = 'showPill',
|
||||
asTitle = 'asTitle',
|
||||
noOpenTrades = 'noOpenTrades',
|
||||
}
|
||||
|
||||
export enum SettingsGetters {
|
||||
openTradesInTitle = 'openTradesInTitle',
|
||||
timezone = 'timezone',
|
||||
backgroundSync = 'backgroundSync',
|
||||
}
|
||||
|
||||
export enum SettingsActions {
|
||||
setOpenTradesInTitle = 'setOpenTradesInTitle',
|
||||
setTimeZone = 'setTimeZone',
|
||||
setBackgroundSync = 'setBackgroundSync',
|
||||
}
|
||||
|
||||
export enum SettingsMutations {
|
||||
setOpenTrades = 'setOpenTrades',
|
||||
setTimeZone = 'setTimeZone',
|
||||
setBackgroundSync = 'setBackgroundSync',
|
||||
}
|
||||
|
||||
export interface SettingsType {
|
||||
openTradesInTitle: string;
|
||||
timezone: string;
|
||||
backgroundSync: boolean;
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
const fromStore = localStorage.getItem(STORE_UI_SETTINGS);
|
||||
if (fromStore) {
|
||||
return JSON.parse(fromStore);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const storedSettings = getSettings();
|
||||
|
||||
function updateSetting(key: string, value: string | boolean) {
|
||||
const settings = getSettings() || {};
|
||||
settings[key] = value;
|
||||
localStorage.setItem(STORE_UI_SETTINGS, JSON.stringify(settings));
|
||||
}
|
||||
|
||||
const state: SettingsType = {
|
||||
openTradesInTitle: storedSettings?.openTradesInTitle || OpenTradeVizOptions.showPill,
|
||||
timezone: storedSettings.timezone || 'UTC',
|
||||
backgroundSync: storedSettings.backgroundSync || true,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters: {
|
||||
[SettingsGetters.openTradesInTitle](state): string {
|
||||
return state.openTradesInTitle;
|
||||
},
|
||||
[SettingsGetters.timezone](state): string {
|
||||
return state.timezone;
|
||||
},
|
||||
[SettingsGetters.backgroundSync](state): boolean {
|
||||
return state.backgroundSync;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[SettingsMutations.setOpenTrades](state, value: string) {
|
||||
state.openTradesInTitle = value;
|
||||
updateSetting('openTradesInTitle', value);
|
||||
},
|
||||
[SettingsMutations.setTimeZone](state, timezone: string) {
|
||||
state.timezone = timezone;
|
||||
updateSetting('timezone', timezone);
|
||||
},
|
||||
[SettingsMutations.setBackgroundSync](state, backgroundSync: boolean) {
|
||||
state.backgroundSync = backgroundSync;
|
||||
updateSetting('backgroundSync', backgroundSync);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[SettingsActions.setOpenTradesInTitle]({ commit }, locked: boolean) {
|
||||
commit(SettingsMutations.setOpenTrades, locked);
|
||||
},
|
||||
[SettingsActions.setTimeZone]({ commit }, timezone: string) {
|
||||
setTimezone(timezone);
|
||||
commit(SettingsMutations.setTimeZone, timezone);
|
||||
},
|
||||
[SettingsActions.setBackgroundSync]({ commit }, timezone: string) {
|
||||
commit(SettingsMutations.setBackgroundSync, timezone);
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
enum StoreModules {
|
||||
ftbot = 'ftbot',
|
||||
alerts = 'alerts',
|
||||
layout = 'layout',
|
||||
uiSettings = 'uiSettings',
|
||||
}
|
||||
|
||||
export default StoreModules;
|
21
src/stores/alerts.ts
Normal file
21
src/stores/alerts.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { AlertType } from '@/types/alertTypes';
|
||||
|
||||
export const useAlertsStore = defineStore('alerts', {
|
||||
state: () => {
|
||||
return { activeMessages: [] as AlertType[] };
|
||||
},
|
||||
actions: {
|
||||
addAlert(message: AlertType) {
|
||||
this.activeMessages.push(message);
|
||||
},
|
||||
removeAlert() {
|
||||
this.activeMessages.shift();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function showAlert(message: string, severity = '') {
|
||||
const alertsStore = useAlertsStore();
|
||||
alertsStore.addAlert({ message, severity });
|
||||
}
|
811
src/stores/ftbot.ts
Normal file
811
src/stores/ftbot.ts
Normal file
|
@ -0,0 +1,811 @@
|
|||
import { useApi } from '@/shared/apiService';
|
||||
import {
|
||||
getAllPlotConfigNames,
|
||||
getPlotConfigName,
|
||||
storeCustomPlotConfig,
|
||||
storePlotConfigName,
|
||||
} from '@/shared/storage';
|
||||
import { useUserService } from '@/shared/userService';
|
||||
import { parseParams } from '@/shared/apiParamParser';
|
||||
import {
|
||||
BotState,
|
||||
Trade,
|
||||
PlotConfig,
|
||||
StrategyResult,
|
||||
BalanceInterface,
|
||||
DailyReturnValue,
|
||||
LockResponse,
|
||||
PlotConfigStorage,
|
||||
ProfitInterface,
|
||||
BacktestResult,
|
||||
StrategyBacktestResult,
|
||||
BacktestSteps,
|
||||
LogLine,
|
||||
SysInfoResponse,
|
||||
LoadingStatus,
|
||||
BacktestHistoryEntry,
|
||||
RunModes,
|
||||
EMPTY_PLOTCONFIG,
|
||||
DailyPayload,
|
||||
BlacklistResponse,
|
||||
WhitelistResponse,
|
||||
StrategyListResult,
|
||||
AvailablePairPayload,
|
||||
AvailablePairResult,
|
||||
PairHistoryPayload,
|
||||
PairCandlePayload,
|
||||
StatusResponse,
|
||||
ForceSellPayload,
|
||||
DeleteTradeResponse,
|
||||
BacktestStatus,
|
||||
BacktestPayload,
|
||||
BlacklistPayload,
|
||||
ForceEnterPayload,
|
||||
TradeResponse,
|
||||
} from '@/types';
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { defineStore } from 'pinia';
|
||||
import { showAlert } from './alerts';
|
||||
|
||||
export function createBotSubStore(botId: string, botName: string) {
|
||||
const userService = useUserService(botId);
|
||||
const { api } = useApi(userService, botId);
|
||||
|
||||
const useBotStore = defineStore(botId, {
|
||||
state: () => {
|
||||
return {
|
||||
ping: '',
|
||||
botStatusAvailable: false,
|
||||
isBotOnline: false,
|
||||
autoRefresh: false,
|
||||
refreshing: false,
|
||||
versionState: '',
|
||||
lastLogs: [] as LogLine[],
|
||||
refreshRequired: true,
|
||||
trades: [] as Trade[],
|
||||
openTrades: [] as Trade[],
|
||||
tradeCount: 0,
|
||||
performanceStats: [] as Performance[],
|
||||
whitelist: [] as string[],
|
||||
blacklist: [] as string[],
|
||||
profit: {} as ProfitInterface,
|
||||
botState: {} as BotState,
|
||||
balance: {} as BalanceInterface,
|
||||
dailyStats: {} as DailyReturnValue,
|
||||
pairlistMethods: [] as string[],
|
||||
detailTradeId: null as number | null,
|
||||
selectedPair: '',
|
||||
// TODO: type me
|
||||
candleData: {},
|
||||
candleDataStatus: LoadingStatus.loading,
|
||||
// TODO: type me
|
||||
history: {},
|
||||
historyStatus: LoadingStatus.loading,
|
||||
strategyPlotConfig: undefined as PlotConfig | undefined,
|
||||
customPlotConfig: {} as PlotConfigStorage,
|
||||
plotConfigName: getPlotConfigName(),
|
||||
availablePlotConfigNames: getAllPlotConfigNames(),
|
||||
strategyList: [] as string[],
|
||||
strategy: {} as StrategyResult,
|
||||
pairlist: [] as string[],
|
||||
currentLocks: undefined as LockResponse | undefined,
|
||||
// backtesting
|
||||
backtestRunning: false,
|
||||
backtestProgress: 0.0,
|
||||
backtestStep: BacktestSteps.none,
|
||||
backtestTradeCount: 0,
|
||||
backtestResult: undefined as BacktestResult | undefined,
|
||||
selectedBacktestResultKey: '',
|
||||
backtestHistory: {} as Record<string, StrategyBacktestResult>,
|
||||
backtestHistoryList: [] as BacktestHistoryEntry[],
|
||||
sysInfo: {} as SysInfoResponse,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
version: (state) => state.botState?.version || state.versionState,
|
||||
botApiVersion: (state) => state.botState?.api_version || 1.0,
|
||||
stakeCurrency: (state) => state.botState?.stake_currency || '',
|
||||
stakeCurrencyDecimals: (state) => state.botState?.stake_currency_decimals || 3,
|
||||
canRunBacktest: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
||||
isWebserverMode: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
||||
selectedBacktestResult: (state) => state.backtestHistory[state.selectedBacktestResultKey],
|
||||
shortAllowed: (state) => state.botState?.short_allowed || false,
|
||||
openTradeCount: (state) => state.openTrades.length,
|
||||
isTrading: (state) =>
|
||||
state.botState?.runmode === RunModes.LIVE || state.botState?.runmode === RunModes.DRY_RUN,
|
||||
timeframe: (state) => state.botState?.timeframe || '',
|
||||
closedTrades: (state) => {
|
||||
return state.trades
|
||||
.filter((item) => !item.is_open)
|
||||
.sort((a, b) =>
|
||||
// Sort by close timestamp, then by tradeid
|
||||
b.close_timestamp && a.close_timestamp
|
||||
? b.close_timestamp - a.close_timestamp
|
||||
: b.trade_id - a.trade_id,
|
||||
);
|
||||
},
|
||||
tradeDetail: (state) => {
|
||||
let dTrade = state.openTrades.find((item) => item.trade_id === state.detailTradeId);
|
||||
if (!dTrade) {
|
||||
dTrade = state.trades.find((item) => item.trade_id === state.detailTradeId);
|
||||
}
|
||||
return dTrade;
|
||||
},
|
||||
plotConfig: (state) =>
|
||||
state.customPlotConfig[state.plotConfigName] || { ...EMPTY_PLOTCONFIG },
|
||||
refreshNow: (state) => {
|
||||
if (
|
||||
state.autoRefresh &&
|
||||
state.isBotOnline &&
|
||||
state.botStatusAvailable &&
|
||||
state.botState?.runmode !== RunModes.WEBSERVER
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// TODO: This backgroundSyncCheck is still missing above
|
||||
// const bgRefresh = rootGetters['uiSettings/backgroundSync'];
|
||||
// const selectedBot = rootGetters[`${StoreModules.ftbot}/selectedBot`];
|
||||
// if (
|
||||
// (selectedBot === botId || bgRefresh) &&
|
||||
// getters.autoRefresh &&
|
||||
// getters.isBotOnline &&
|
||||
// getters.botStatusAvailable &&
|
||||
// !getters.isWebserverMode
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
return false;
|
||||
},
|
||||
botName: (state) => state.botState?.bot_name || 'freqtrade',
|
||||
allTrades: (state) => [...state.openTrades, ...state.trades] as Trade[],
|
||||
activeLocks: (state) => state.currentLocks?.locks || [],
|
||||
},
|
||||
actions: {
|
||||
botAdded() {
|
||||
this.autoRefresh = userService.getAutoRefresh();
|
||||
},
|
||||
async fetchPing() {
|
||||
try {
|
||||
const result = await api.get('/ping');
|
||||
const now = Date.now();
|
||||
// TODO: Name collision!
|
||||
this.ping = `${result.data.status} ${now.toString()}`;
|
||||
this.isBotOnline = true;
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
console.log('ping fail');
|
||||
this.isBotOnline = false;
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
userService.logout();
|
||||
},
|
||||
rename(name: string) {
|
||||
userService.renameBot(name);
|
||||
},
|
||||
setRefreshRequired(refreshRequired: boolean) {
|
||||
this.refreshRequired = refreshRequired;
|
||||
},
|
||||
setAutoRefresh(newRefreshValue) {
|
||||
this.autoRefresh = newRefreshValue;
|
||||
// TODO: Investigate this -
|
||||
// this ONLY works if ReloadControl is only visible once,otherwise it triggers twice
|
||||
if (newRefreshValue) {
|
||||
// dispatch('startRefresh', true);
|
||||
} else {
|
||||
// dispatch('stopRefresh');
|
||||
}
|
||||
userService.setAutoRefresh(newRefreshValue);
|
||||
},
|
||||
setIsBotOnline(isBotOnline: boolean) {
|
||||
this.isBotOnline = isBotOnline;
|
||||
},
|
||||
async refreshSlow(forceUpdate = false) {
|
||||
if (this.refreshing && !forceUpdate) {
|
||||
return;
|
||||
}
|
||||
// Refresh data only when needed
|
||||
if (forceUpdate || this.refreshRequired) {
|
||||
// TODO: Should be AxiosInstance
|
||||
const updates: Promise<any>[] = [];
|
||||
updates.push(this.getPerformance());
|
||||
updates.push(this.getProfit());
|
||||
updates.push(this.getTrades());
|
||||
updates.push(this.getBalance());
|
||||
// /* white/blacklist might be refreshed more often as they are not expensive on the backend */
|
||||
updates.push(this.getWhitelist());
|
||||
updates.push(this.getBlacklist());
|
||||
await Promise.all(updates);
|
||||
this.refreshRequired = false;
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
async refreshFrequent() {
|
||||
// Refresh data that's needed in near realtime
|
||||
await this.getOpenTrades();
|
||||
await this.getState();
|
||||
await this.getLocks();
|
||||
},
|
||||
|
||||
setDetailTrade(trade: Trade | null) {
|
||||
this.detailTradeId = trade?.trade_id || null;
|
||||
this.selectedPair = trade ? trade.pair : this.selectedPair;
|
||||
},
|
||||
setSelectedPair(pair: string) {
|
||||
this.selectedPair = pair;
|
||||
},
|
||||
saveCustomPlotConfig(plotConfig: PlotConfigStorage) {
|
||||
this.customPlotConfig = plotConfig;
|
||||
storeCustomPlotConfig(plotConfig);
|
||||
this.availablePlotConfigNames = getAllPlotConfigNames();
|
||||
},
|
||||
updatePlotConfigName(plotConfigName: string) {
|
||||
// Set default plot config name
|
||||
this.plotConfigName = plotConfigName;
|
||||
storePlotConfigName(plotConfigName);
|
||||
},
|
||||
setPlotConfigName(plotConfigName: string) {
|
||||
// TODO: This is identical to updatePlotConfigName
|
||||
this.plotConfigName = plotConfigName;
|
||||
storePlotConfigName(plotConfigName);
|
||||
},
|
||||
async getTrades() {
|
||||
try {
|
||||
let totalTrades = 0;
|
||||
const pageLength = 500;
|
||||
const fetchTrades = async (limit: number, offset: number) => {
|
||||
return api.get<TradeResponse>('/trades', {
|
||||
params: { limit, offset },
|
||||
});
|
||||
};
|
||||
const res = await fetchTrades(pageLength, 0);
|
||||
const result: TradeResponse = res.data;
|
||||
let { trades } = result;
|
||||
if (Array.isArray(trades) && trades.length !== result.total_trades) {
|
||||
// Pagination necessary
|
||||
// Don't use Promise.all - this would fire all requests at once, which can
|
||||
// cause problems for big sqlite databases
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const res = await fetchTrades(pageLength, trades.length);
|
||||
|
||||
const result: TradeResponse = res.data;
|
||||
trades = trades.concat(result.trades);
|
||||
totalTrades = res.data.total_trades;
|
||||
} while (trades.length !== totalTrades);
|
||||
const tradesCount = trades.length;
|
||||
// Add botId to all trades
|
||||
trades = trades.map((t) => ({
|
||||
...t,
|
||||
botId,
|
||||
botName,
|
||||
botTradeId: `${botId}__${t.trade_id}`,
|
||||
}));
|
||||
this.trades = trades;
|
||||
this.tradeCount = tradesCount;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
getOpenTrades() {
|
||||
return api
|
||||
.get<never, AxiosResponse<Trade[]>>('/status')
|
||||
.then((result) => {
|
||||
// Check if trade-id's are different in this call, then trigger a full refresh
|
||||
if (
|
||||
Array.isArray(this.openTrades) &&
|
||||
Array.isArray(result.data) &&
|
||||
(this.openTrades.length !== result.data.length ||
|
||||
!this.openTrades.every(
|
||||
(val, index) => val.trade_id === result.data[index].trade_id,
|
||||
))
|
||||
) {
|
||||
// Open trades changed, so we should refresh now.
|
||||
this.refreshRequired = true;
|
||||
// dispatch('refreshSlow', null, { root: true });
|
||||
|
||||
const openTrades = result.data.map((t) => ({
|
||||
...t,
|
||||
botId,
|
||||
botName,
|
||||
botTradeId: `${botId}__${t.trade_id}`,
|
||||
}));
|
||||
this.openTrades = openTrades;
|
||||
} else {
|
||||
this.openTrades = [];
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
getLocks() {
|
||||
return api
|
||||
.get('/locks')
|
||||
.then((result) => (this.currentLocks = result.data))
|
||||
.catch(console.error);
|
||||
},
|
||||
async deleteLock(lockid: string) {
|
||||
try {
|
||||
const res = await api.delete<LockResponse>(`/locks/${lockid}`);
|
||||
showAlert(`Deleted Lock ${lockid}.`);
|
||||
this.currentLocks = res.data;
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert(`Failed to delete lock ${lockid}`, 'danger');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
getPairCandles(payload: PairCandlePayload) {
|
||||
if (payload.pair && payload.timeframe) {
|
||||
this.historyStatus = LoadingStatus.loading;
|
||||
return api
|
||||
.get('/pair_candles', {
|
||||
params: { ...payload },
|
||||
})
|
||||
.then((result) => {
|
||||
this.candleData = {
|
||||
...this.candleData,
|
||||
[`${payload.pair}__${payload.timeframe}`]: {
|
||||
pair: payload.pair,
|
||||
timeframe: payload.timeframe,
|
||||
data: result.data,
|
||||
},
|
||||
};
|
||||
this.historyStatus = LoadingStatus.success;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
this.historyStatus = LoadingStatus.error;
|
||||
});
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'pair or timeframe not specified';
|
||||
console.error(error);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
getPairHistory(payload: PairHistoryPayload) {
|
||||
if (payload.pair && payload.timeframe && payload.timerange) {
|
||||
this.historyStatus = LoadingStatus.loading;
|
||||
return api
|
||||
.get('/pair_history', {
|
||||
params: { ...payload },
|
||||
timeout: 50000,
|
||||
})
|
||||
.then((result) => {
|
||||
this.history = {
|
||||
[`${payload.pair}__${payload.timeframe}`]: {
|
||||
pair: payload.pair,
|
||||
timeframe: payload.timeframe,
|
||||
timerange: payload.timerange,
|
||||
data: result.data,
|
||||
},
|
||||
};
|
||||
this.historyStatus = LoadingStatus.success;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
this.historyStatus = LoadingStatus.error;
|
||||
});
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'pair or timeframe or timerange not specified';
|
||||
console.error(error);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
async getStrategyPlotConfig() {
|
||||
try {
|
||||
const result = await api.get<PlotConfig>('/plot_config');
|
||||
const plotConfig = result.data;
|
||||
if (plotConfig.subplots === null) {
|
||||
// Subplots should not be null but an empty object
|
||||
// TODO: Remove this fix when fix in freqtrade is populated further.
|
||||
plotConfig.subplots = {};
|
||||
}
|
||||
this.strategyPlotConfig = result.data;
|
||||
return Promise.resolve();
|
||||
} catch (data) {
|
||||
console.error(data);
|
||||
return Promise.reject(data);
|
||||
}
|
||||
},
|
||||
getStrategyList() {
|
||||
return api
|
||||
.get<StrategyListResult>('/strategies')
|
||||
.then((result) => (this.strategyList = result.data.strategies))
|
||||
.catch(console.error);
|
||||
},
|
||||
async getStrategy(strategy: string) {
|
||||
try {
|
||||
const result = await api.get<StrategyResult>(`/strategy/${strategy}`, {});
|
||||
this.strategy = result.data;
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async getAvailablePairs(payload: AvailablePairPayload) {
|
||||
try {
|
||||
const result = await api.get<AvailablePairResult>('/available_pairs', {
|
||||
params: { ...payload },
|
||||
});
|
||||
// result is of type AvailablePairResult
|
||||
const { pairs } = result.data;
|
||||
this.pairlist = pairs;
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async getPerformance() {
|
||||
try {
|
||||
const result = await api.get<Performance[]>('/performance');
|
||||
this.performanceStats = result.data;
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
getWhitelist() {
|
||||
return api
|
||||
.get<WhitelistResponse>('/whitelist')
|
||||
.then((result) => {
|
||||
this.whitelist = result.data.whitelist;
|
||||
this.pairlistMethods = result.data.method;
|
||||
return Promise.resolve(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
// console.error(error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
getBlacklist() {
|
||||
return api
|
||||
.get<BlacklistResponse>('/blacklist')
|
||||
.then((result) => (this.blacklist = result.data.blacklist))
|
||||
.catch(console.error);
|
||||
},
|
||||
getProfit() {
|
||||
return api
|
||||
.get('/profit')
|
||||
.then((result) => (this.profit = result.data))
|
||||
.catch(console.error);
|
||||
},
|
||||
async getBalance() {
|
||||
try {
|
||||
const result = await api.get('/balance');
|
||||
this.balance = result.data;
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async getDaily(payload: DailyPayload = {}) {
|
||||
const { timescale = 20 } = payload;
|
||||
try {
|
||||
const { data } = await api.get<DailyReturnValue>('/daily', { params: { timescale } });
|
||||
this.dailyStats = data;
|
||||
return Promise.resolve(data);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
getState() {
|
||||
return api
|
||||
.get('/show_config')
|
||||
.then((result) => {
|
||||
this.botState = result.data;
|
||||
this.botStatusAvailable = true;
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
getLogs() {
|
||||
return api
|
||||
.get('/logs')
|
||||
.then((result) => (this.lastLogs = result.data.logs))
|
||||
.catch(console.error);
|
||||
},
|
||||
// // Post methods
|
||||
// // TODO: Migrate calls to API to a seperate module unrelated to pinia?
|
||||
async startBot() {
|
||||
try {
|
||||
const res = await api.post<{}, AxiosResponse<StatusResponse>>('/start', {});
|
||||
console.log(res.data);
|
||||
showAlert(res.data.status);
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert('Error starting bot.');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async stopBot() {
|
||||
try {
|
||||
const res = await api.post<{}, AxiosResponse<StatusResponse>>('/stop', {});
|
||||
showAlert(res.data.status);
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert('Error stopping bot.');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async stopBuy() {
|
||||
try {
|
||||
const res = await api.post<{}, AxiosResponse<StatusResponse>>('/stopbuy', {});
|
||||
showAlert(res.data.status);
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert('Error calling stopbuy.');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async reloadConfig() {
|
||||
try {
|
||||
const res = await api.post<{}, AxiosResponse<StatusResponse>>('/reload_config', {});
|
||||
console.log(res.data);
|
||||
showAlert(res.data.status);
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert('Error reloading.');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async deleteTrade(tradeid: string) {
|
||||
try {
|
||||
const res = await api.delete<DeleteTradeResponse>(`/trades/${tradeid}`);
|
||||
showAlert(res.data.result_msg ? res.data.result_msg : `Deleted Trade ${tradeid}`);
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert(`Failed to delete trade ${tradeid}`, 'danger');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async startTrade() {
|
||||
try {
|
||||
const res = await api.post('/start_trade', {});
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async forceexit(payload: ForceSellPayload) {
|
||||
try {
|
||||
const res = await api.post<ForceSellPayload, AxiosResponse<StatusResponse>>(
|
||||
'/forcesell',
|
||||
payload,
|
||||
);
|
||||
showAlert(`Sell order for ${payload.tradeid} created`);
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
}
|
||||
showAlert(`Failed to create sell order for ${payload.tradeid}`, 'danger');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async forcebuy(payload: ForceEnterPayload) {
|
||||
if (payload && payload.pair) {
|
||||
try {
|
||||
// TODO: Update forcebuy to forceenter ...
|
||||
const res = await api.post<
|
||||
ForceEnterPayload,
|
||||
AxiosResponse<StatusResponse | TradeResponse>
|
||||
>('/forcebuy', payload);
|
||||
showAlert(`Order for ${payload.pair} created.`);
|
||||
|
||||
return Promise.resolve(res);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
showAlert(
|
||||
`Error occured entering: '${(error as any).response?.data?.error}'`,
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'Pair is empty';
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
async addBlacklist(payload: BlacklistPayload) {
|
||||
console.log(`Adding ${payload} to blacklist`);
|
||||
if (payload && payload.blacklist) {
|
||||
try {
|
||||
const result = await api.post<BlacklistPayload, AxiosResponse<BlacklistResponse>>(
|
||||
'/blacklist',
|
||||
payload,
|
||||
);
|
||||
this.blacklist = result.data.blacklist;
|
||||
if (result.data.errors && Object.keys(result.data.errors).length !== 0) {
|
||||
const { errors } = result.data;
|
||||
Object.keys(errors).forEach((pair) => {
|
||||
showAlert(
|
||||
`Error while adding pair ${pair} to Blacklist: ${errors[pair].error_msg}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
showAlert(`Pair ${payload.blacklist} added.`);
|
||||
}
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
showAlert(
|
||||
`Error occured while adding pairs to Blacklist: '${
|
||||
(error as any).response?.data?.error
|
||||
}'`,
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'Pair is empty';
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
async deleteBlacklist(blacklistPairs: Array<string>) {
|
||||
console.log(`Deleting ${blacklistPairs} from blacklist.`);
|
||||
|
||||
if (blacklistPairs) {
|
||||
try {
|
||||
const result = await api.delete<BlacklistPayload, AxiosResponse<BlacklistResponse>>(
|
||||
'/blacklist',
|
||||
{
|
||||
params: {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pairs_to_delete: blacklistPairs,
|
||||
},
|
||||
paramsSerializer: (params) => parseParams(params),
|
||||
},
|
||||
);
|
||||
this.blacklist = result.data.blacklist;
|
||||
if (result.data.errors && Object.keys(result.data.errors).length !== 0) {
|
||||
const { errors } = result.data;
|
||||
Object.keys(errors).forEach((pair) => {
|
||||
showAlert(
|
||||
`Error while removing pair ${pair} from Blacklist: ${errors[pair].error_msg}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
showAlert(`Pair ${blacklistPairs} removed.`);
|
||||
}
|
||||
return Promise.resolve(result.data);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.response);
|
||||
showAlert(
|
||||
`Error occured while removing pairs from Blacklist: '${
|
||||
(error as any).response?.data?.error
|
||||
}'`,
|
||||
'danger',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
// Error branchs
|
||||
const error = 'Pair is empty';
|
||||
console.error(error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
async startBacktest(payload: BacktestPayload) {
|
||||
try {
|
||||
const result = await api.post<BacktestPayload, AxiosResponse<BacktestStatus>>(
|
||||
'/backtest',
|
||||
payload,
|
||||
);
|
||||
this.updateBacktestRunning(result.data);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
async pollBacktest() {
|
||||
const result = await api.get<BacktestStatus>('/backtest');
|
||||
|
||||
this.updateBacktestRunning(result.data);
|
||||
if (result.data.running === false && result.data.backtest_result) {
|
||||
this.updateBacktestResult(result.data.backtest_result);
|
||||
}
|
||||
},
|
||||
async removeBacktest() {
|
||||
this.backtestHistory = {};
|
||||
try {
|
||||
const { data } = await api.delete<BacktestStatus>('/backtest');
|
||||
this.updateBacktestRunning(data);
|
||||
return Promise.resolve(data);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
updateBacktestRunning(backtestStatus: BacktestStatus) {
|
||||
this.backtestRunning = backtestStatus.running;
|
||||
this.backtestProgress = backtestStatus.progress;
|
||||
this.backtestStep = backtestStatus.step;
|
||||
this.backtestTradeCount = backtestStatus.trade_count || 0;
|
||||
},
|
||||
async stopBacktest() {
|
||||
try {
|
||||
const { data } = await api.get<BacktestStatus>('/backtest/abort');
|
||||
this.updateBacktestRunning(data);
|
||||
return Promise.resolve(data);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
async getBacktestHistory() {
|
||||
const result = await api.get<BacktestHistoryEntry[]>('/backtest/history');
|
||||
this.backtestHistoryList = result.data;
|
||||
},
|
||||
updateBacktestResult(backtestResult: BacktestResult) {
|
||||
this.backtestResult = backtestResult;
|
||||
// TODO: Properly identify duplicates to avoid pushing the same multiple times
|
||||
Object.entries(backtestResult.strategy).forEach(([key, strat]) => {
|
||||
console.log(key, strat);
|
||||
|
||||
const stratKey = `${key}_${strat.total_trades}_${strat.profit_total.toFixed(3)}`;
|
||||
// this.backtestHistory[stratKey] = strat;
|
||||
this.backtestHistory = { ...this.backtestHistory, ...{ [stratKey]: strat } };
|
||||
this.selectedBacktestResultKey = stratKey;
|
||||
});
|
||||
},
|
||||
async getBacktestHistoryResult(payload: BacktestHistoryEntry) {
|
||||
const result = await api.get<BacktestStatus>('/backtest/history/result', {
|
||||
params: { filename: payload.filename, strategy: payload.strategy },
|
||||
});
|
||||
if (result.data.backtest_result) {
|
||||
this.updateBacktestResult(result.data.backtest_result);
|
||||
}
|
||||
},
|
||||
setBacktestResultKey(key: string) {
|
||||
this.selectedBacktestResultKey = key;
|
||||
},
|
||||
async getSysInfo() {
|
||||
try {
|
||||
const { data } = await api.get<SysInfoResponse>('/sysinfo');
|
||||
this.sysInfo = data;
|
||||
return Promise.resolve(data);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
return useBotStore();
|
||||
}
|
317
src/stores/ftbotwrapper.ts
Normal file
317
src/stores/ftbotwrapper.ts
Normal file
|
@ -0,0 +1,317 @@
|
|||
import { UserService } from '@/shared/userService';
|
||||
import {
|
||||
BalanceInterface,
|
||||
BotDescriptor,
|
||||
BotDescriptors,
|
||||
BotState,
|
||||
DailyPayload,
|
||||
DailyRecord,
|
||||
DailyReturnValue,
|
||||
MultiDeletePayload,
|
||||
MultiForcesellPayload,
|
||||
ProfitInterface,
|
||||
RenameBotPayload,
|
||||
Trade,
|
||||
} from '@/types';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { defineStore } from 'pinia';
|
||||
import { createBotSubStore } from './ftbot';
|
||||
const AUTH_SELECTED_BOT = 'ftSelectedBot';
|
||||
|
||||
export type BotSubStore = ReturnType<typeof createBotSubStore>;
|
||||
|
||||
export interface SubStores {
|
||||
[key: string]: BotSubStore;
|
||||
}
|
||||
|
||||
export const useBotStore = defineStore('wrapper', {
|
||||
state: () => {
|
||||
return {
|
||||
selectedBot: '',
|
||||
availableBots: {} as BotDescriptors,
|
||||
globalAutoRefresh: true,
|
||||
refreshing: false,
|
||||
refreshInterval: null as number | null,
|
||||
refreshIntervalSlow: null as number | null,
|
||||
botStores: {} as SubStores,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
hasBots: (state) => Object.keys(state.availableBots).length > 0,
|
||||
botCount: (state) => Object.keys(state.availableBots).length,
|
||||
allBotStores: (state) => Object.values(state.botStores),
|
||||
activeBot: (state) => state.botStores[state.selectedBot] as BotSubStore,
|
||||
activeBotorUndefined: (state) => state.botStores[state.selectedBot] as BotSubStore | undefined,
|
||||
canRunBacktest: (state) => state.botStores[state.selectedBot]?.canRunBacktest ?? false,
|
||||
selectedBotObj: (state) => state.availableBots[state.selectedBot],
|
||||
nextBotId: (state) => {
|
||||
let botCount = Object.keys(state.availableBots).length;
|
||||
|
||||
while (`ftbot.${botCount}` in state.availableBots) {
|
||||
botCount += 1;
|
||||
}
|
||||
return `ftbot.${botCount}`;
|
||||
},
|
||||
allProfit: (state): Record<string, ProfitInterface> => {
|
||||
const result: Record<string, ProfitInterface> = {};
|
||||
Object.entries(state.botStores).forEach(([k, botStore]) => {
|
||||
result[k] = botStore.profit;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
allOpenTradeCount: (state): Record<string, number> => {
|
||||
const result: Record<string, number> = {};
|
||||
Object.entries(state.botStores).forEach(([k, botStore]) => {
|
||||
result[k] = botStore.openTradeCount;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
allOpenTrades: (state): Record<string, Trade[]> => {
|
||||
const result: Record<string, Trade[]> = {};
|
||||
Object.entries(state.botStores).forEach(([k, botStore]) => {
|
||||
result[k] = botStore.openTrades;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
allBalance: (state): Record<string, BalanceInterface> => {
|
||||
const result: Record<string, BalanceInterface> = {};
|
||||
Object.entries(state.botStores).forEach(([k, botStore]) => {
|
||||
result[k] = botStore.balance;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
allBotState: (state): Record<string, BotState> => {
|
||||
const result: Record<string, BotState> = {};
|
||||
Object.entries(state.botStores).forEach(([k, botStore]) => {
|
||||
result[k] = botStore.botState;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
allOpenTradesAllBots: (state): Trade[] => {
|
||||
const result: Trade[] = [];
|
||||
Object.entries(state.botStores).forEach(([, botStore]) => {
|
||||
result.push(...botStore.openTrades);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
allTradesAllBots: (state): Trade[] => {
|
||||
const result: Trade[] = [];
|
||||
Object.entries(state.botStores).forEach(([, botStore]) => {
|
||||
result.push(...botStore.trades);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
allDailyStatsAllBots: (state): DailyReturnValue => {
|
||||
// Return aggregated daily stats for all bots - sorted ascending.
|
||||
const resp: Record<string, DailyRecord> = {};
|
||||
Object.entries(state.botStores).forEach(([, botStore]) => {
|
||||
botStore.dailyStats?.data?.forEach((d) => {
|
||||
if (!resp[d.date]) {
|
||||
resp[d.date] = { ...d };
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].abs_profit += d.abs_profit;
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].fiat_value += d.fiat_value;
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].trade_count += d.trade_count;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dailyReturn: DailyReturnValue = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
stake_currency: 'USDT',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
fiat_display_currency: 'USD',
|
||||
data: Object.values(resp).sort((a, b) => (a.date > b.date ? 1 : -1)),
|
||||
};
|
||||
return dailyReturn;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
selectBot(botId: string) {
|
||||
if (botId in this.availableBots) {
|
||||
localStorage.setItem(AUTH_SELECTED_BOT, botId);
|
||||
this.selectedBot = botId;
|
||||
} else {
|
||||
console.warn(`Botid ${botId} not available, but selected.`);
|
||||
}
|
||||
},
|
||||
addBot(bot: BotDescriptor) {
|
||||
if (Object.keys(this.availableBots).includes(bot.botId)) {
|
||||
// throw 'Bot already present';
|
||||
// TODO: handle error!
|
||||
console.log('Bot already present');
|
||||
return;
|
||||
}
|
||||
console.log('add bot', bot);
|
||||
const botStore = createBotSubStore(bot.botId, bot.botName);
|
||||
botStore.botAdded();
|
||||
this.botStores[bot.botId] = botStore;
|
||||
this.availableBots[bot.botId] = bot;
|
||||
this.botStores = { ...this.botStores };
|
||||
this.availableBots = { ...this.availableBots };
|
||||
},
|
||||
renameBot(bot: RenameBotPayload) {
|
||||
if (!Object.keys(this.availableBots).includes(bot.botId)) {
|
||||
// TODO: handle error!
|
||||
console.error('Bot not found');
|
||||
return;
|
||||
}
|
||||
this.botStores[bot.botId].rename(bot.botName);
|
||||
this.availableBots[bot.botId].botName = bot.botName;
|
||||
},
|
||||
removeBot(botId: string) {
|
||||
if (Object.keys(this.availableBots).includes(botId)) {
|
||||
this.botStores[botId].logout();
|
||||
this.botStores[botId].$dispose();
|
||||
|
||||
delete this.botStores[botId];
|
||||
delete this.availableBots[botId];
|
||||
this.botStores = { ...this.botStores };
|
||||
this.availableBots = { ...this.availableBots };
|
||||
// commit('removeBot', botId);
|
||||
} else {
|
||||
console.warn(`bot ${botId} not found! could not remove`);
|
||||
}
|
||||
},
|
||||
|
||||
selectFirstBot() {
|
||||
if (this.hasBots) {
|
||||
const selBotId = localStorage.getItem(AUTH_SELECTED_BOT);
|
||||
const firstBot = Object.keys(this.availableBots)[0];
|
||||
let selBot: string | undefined = firstBot;
|
||||
if (selBotId) {
|
||||
selBot = Object.keys(this.availableBots).find((x) => x === selBotId);
|
||||
}
|
||||
this.selectBot(this.availableBots[selBot || firstBot].botId);
|
||||
}
|
||||
},
|
||||
setGlobalAutoRefresh(value: boolean) {
|
||||
// TODO: could be removed.
|
||||
this.globalAutoRefresh = value;
|
||||
},
|
||||
async allRefreshFrequent(forceUpdate = false) {
|
||||
const updates: Promise<any>[] = [];
|
||||
this.allBotStores.forEach(async (e) => {
|
||||
if (e.refreshNow && (this.globalAutoRefresh || forceUpdate)) {
|
||||
updates.push(e.refreshFrequent());
|
||||
}
|
||||
});
|
||||
await Promise.all(updates);
|
||||
return Promise.resolve();
|
||||
},
|
||||
async allRefreshSlow(forceUpdate = false) {
|
||||
this.allBotStores.forEach(async (e) => {
|
||||
if (e.refreshNow && (this.globalAutoRefresh || forceUpdate)) {
|
||||
await e.refreshSlow(forceUpdate);
|
||||
}
|
||||
});
|
||||
},
|
||||
async allRefreshFull() {
|
||||
if (this.refreshing) {
|
||||
return;
|
||||
}
|
||||
this.refreshing = true;
|
||||
try {
|
||||
// Ensure all bots status is correct.
|
||||
await this.pingAll();
|
||||
|
||||
const botStoreUpdates: Promise<any>[] = [];
|
||||
this.allBotStores.forEach((e) => {
|
||||
if (e.isBotOnline && !e.botStatusAvailable) {
|
||||
botStoreUpdates.push(e.getState());
|
||||
}
|
||||
});
|
||||
await Promise.all(botStoreUpdates);
|
||||
|
||||
const updates: Promise<any>[] = [];
|
||||
updates.push(this.allRefreshFrequent(false));
|
||||
updates.push(this.allRefreshSlow(true));
|
||||
// updates.push(this.getDaily());
|
||||
// updates.push(this.getBalance());
|
||||
await Promise.all(updates);
|
||||
console.log('refreshing_end');
|
||||
} finally {
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
startRefresh() {
|
||||
console.log('Starting automatic refresh.');
|
||||
this.allRefreshFull();
|
||||
if (!this.refreshInterval) {
|
||||
// Set interval for refresh
|
||||
const refreshInterval = window.setInterval(() => {
|
||||
this.allRefreshFrequent();
|
||||
}, 5000);
|
||||
this.refreshInterval = refreshInterval;
|
||||
}
|
||||
if (!this.refreshIntervalSlow) {
|
||||
const refreshIntervalSlow = window.setInterval(() => {
|
||||
this.allRefreshSlow(false);
|
||||
}, 60000);
|
||||
this.refreshIntervalSlow = refreshIntervalSlow;
|
||||
}
|
||||
},
|
||||
stopRefresh() {
|
||||
console.log('Stopping automatic refresh.');
|
||||
if (this.refreshInterval) {
|
||||
window.clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
if (this.refreshIntervalSlow) {
|
||||
window.clearInterval(this.refreshIntervalSlow);
|
||||
this.refreshIntervalSlow = null;
|
||||
}
|
||||
},
|
||||
async pingAll() {
|
||||
await Promise.all(
|
||||
Object.entries(this.botStores).map(async ([_, v]) => {
|
||||
try {
|
||||
await v.fetchPing();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
allGetState() {
|
||||
Object.entries(this.botStores).map(async ([_, v]) => {
|
||||
try {
|
||||
await v.getState();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
});
|
||||
},
|
||||
async allGetDaily(payload: DailyPayload) {
|
||||
const updates: Promise<any>[] = [];
|
||||
|
||||
this.allBotStores.forEach((e) => {
|
||||
if (e.isBotOnline) {
|
||||
updates.push(e.getDaily(payload));
|
||||
}
|
||||
});
|
||||
await Promise.all(updates);
|
||||
},
|
||||
async forceSellMulti(forcesellPayload: MultiForcesellPayload) {
|
||||
return this.botStores[forcesellPayload.botId].forceexit(forcesellPayload);
|
||||
},
|
||||
async deleteTradeMulti(deletePayload: MultiDeletePayload) {
|
||||
return this.botStores[deletePayload.botId].deleteTrade(deletePayload.tradeid);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function initBots() {
|
||||
UserService.migrateLogin();
|
||||
|
||||
const botStore = useBotStore();
|
||||
// This might need to be moved to the parent (?)
|
||||
Object.entries(UserService.getAvailableBots()).forEach(([, v]) => {
|
||||
botStore.addBot(v);
|
||||
});
|
||||
botStore.selectFirstBot();
|
||||
}
|
125
src/stores/layout.ts
Normal file
125
src/stores/layout.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { GridItemData } from 'vue-grid-layout';
|
||||
|
||||
export enum TradeLayout {
|
||||
multiPane = 'g-multiPane',
|
||||
openTrades = 'g-openTrades',
|
||||
tradeHistory = 'g-tradeHistory',
|
||||
tradeDetail = 'g-tradeDetail',
|
||||
chartView = 'g-chartView',
|
||||
}
|
||||
|
||||
export enum DashboardLayout {
|
||||
dailyChart = 'g-dailyChart',
|
||||
botComparison = 'g-botComparison',
|
||||
allOpenTrades = 'g-allOpenTrades',
|
||||
cumChartChart = 'g-cumChartChart',
|
||||
tradesLogChart = 'g-TradesLogChart',
|
||||
}
|
||||
|
||||
// Define default layouts
|
||||
const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 3, h: 35 },
|
||||
{ i: TradeLayout.chartView, x: 3, y: 0, w: 9, h: 14 },
|
||||
{ i: TradeLayout.tradeDetail, x: 3, y: 19, w: 9, h: 6 },
|
||||
{ i: TradeLayout.openTrades, x: 3, y: 14, w: 9, h: 5 },
|
||||
{ i: TradeLayout.tradeHistory, x: 3, y: 25, w: 9, h: 10 },
|
||||
];
|
||||
|
||||
// Currently only multiPane is visible
|
||||
const DEFAULT_TRADING_LAYOUT_SM: GridItemData[] = [
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 12, h: 10 },
|
||||
{ i: TradeLayout.chartView, x: 0, y: 10, w: 12, h: 0 },
|
||||
{ i: TradeLayout.tradeDetail, x: 0, y: 19, w: 12, h: 0 },
|
||||
{ i: TradeLayout.openTrades, x: 0, y: 8, w: 12, h: 0 },
|
||||
{ i: TradeLayout.tradeHistory, x: 0, y: 25, w: 12, h: 0 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 8, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.dailyChart, x: 8, y: 0, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 8, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 8, y: 6, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 12, w: 12, h: 4 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT_SM: GridItemData[] = [
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 12, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 12, h: 8 },
|
||||
{ i: DashboardLayout.dailyChart, x: 0, y: 14, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 0, y: 20, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 26, w: 12, h: 4 },
|
||||
];
|
||||
|
||||
const STORE_LAYOUTS = 'ftLayoutSettings';
|
||||
|
||||
function migrateLayoutSettings() {
|
||||
const STORE_DASHBOARD_LAYOUT = 'ftDashboardLayout';
|
||||
const STORE_TRADING_LAYOUT = 'ftTradingLayout';
|
||||
const STORE_LAYOUT_LOCK = 'ftLayoutLocked';
|
||||
|
||||
// If new does not exist
|
||||
if (localStorage.getItem(STORE_LAYOUTS) === null) {
|
||||
console.log('Migrating dashboard settings');
|
||||
const layoutLocked = localStorage.getItem(STORE_LAYOUT_LOCK);
|
||||
const tradingLayout = localStorage.getItem(STORE_TRADING_LAYOUT);
|
||||
const dashboardLayout = localStorage.getItem(STORE_DASHBOARD_LAYOUT);
|
||||
|
||||
const res = {
|
||||
dashboardLayout,
|
||||
tradingLayout,
|
||||
layoutLocked,
|
||||
};
|
||||
localStorage.setItem(STORE_LAYOUTS, JSON.stringify(res));
|
||||
}
|
||||
localStorage.removeItem(STORE_LAYOUT_LOCK);
|
||||
localStorage.removeItem(STORE_TRADING_LAYOUT);
|
||||
localStorage.removeItem(STORE_DASHBOARD_LAYOUT);
|
||||
}
|
||||
migrateLayoutSettings();
|
||||
/**
|
||||
* Helper function finding a layout entry
|
||||
* @param gridLayout Array of grid layouts used in this layout. Must be passed to GridLayout, too.
|
||||
* @param name Name within the dashboard layout to find
|
||||
*/
|
||||
export function findGridLayout(gridLayout: GridItemData[], name: string): GridItemData {
|
||||
let layout = gridLayout.find((value) => value.i === name);
|
||||
if (!layout) {
|
||||
layout = { i: name, x: 0, y: 0, w: 4, h: 6 };
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
export const useLayoutStore = defineStore('layoutStore', {
|
||||
state: () => {
|
||||
return {
|
||||
dashboardLayout: JSON.parse(JSON.stringify(DEFAULT_DASHBOARD_LAYOUT)),
|
||||
tradingLayout: JSON.parse(JSON.stringify(DEFAULT_TRADING_LAYOUT)),
|
||||
layoutLocked: true,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
getDashboardLayoutSm: () => [...DEFAULT_DASHBOARD_LAYOUT_SM],
|
||||
getTradingLayoutSm: () => [...DEFAULT_TRADING_LAYOUT_SM],
|
||||
},
|
||||
actions: {
|
||||
resetTradingLayout() {
|
||||
this.tradingLayout = JSON.parse(JSON.stringify(DEFAULT_TRADING_LAYOUT));
|
||||
},
|
||||
resetDashboardLayout() {
|
||||
this.dashboardLayout = JSON.parse(JSON.stringify(DEFAULT_DASHBOARD_LAYOUT));
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
key: STORE_LAYOUTS,
|
||||
afterRestore: (context) => {
|
||||
console.log('after restore - ', context.store);
|
||||
if (context.store.dashboardLayout === null) {
|
||||
context.store.dashboardLayout = JSON.parse(JSON.stringify(DEFAULT_DASHBOARD_LAYOUT));
|
||||
}
|
||||
if (context.store.tradingLayout === null) {
|
||||
context.store.tradingLayout = JSON.parse(JSON.stringify(DEFAULT_TRADING_LAYOUT));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
73
src/stores/settings.ts
Normal file
73
src/stores/settings.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { defineStore } from 'pinia';
|
||||
|
||||
import { setTimezone } from '@/shared/formatters';
|
||||
import { getCurrentTheme, getTheme } from '@/shared/themes';
|
||||
import axios from 'axios';
|
||||
import { UiVersion } from '@/types';
|
||||
|
||||
const STORE_UI_SETTINGS = 'ftUISettings';
|
||||
|
||||
export enum OpenTradeVizOptions {
|
||||
showPill = 'showPill',
|
||||
asTitle = 'asTitle',
|
||||
noOpenTrades = 'noOpenTrades',
|
||||
}
|
||||
|
||||
export interface SettingsType {
|
||||
openTradesInTitle?: string;
|
||||
timezone?: string;
|
||||
backgroundSync?: boolean;
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('uiSettings', {
|
||||
// other options...
|
||||
state: () => {
|
||||
return {
|
||||
openTradesInTitle: OpenTradeVizOptions.showPill as string,
|
||||
timezone: 'UTC',
|
||||
backgroundSync: true,
|
||||
// TODO: needs proper migration ...
|
||||
currentTheme: getCurrentTheme(),
|
||||
uiVersion: 'dev',
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
isDarkTheme(state) {
|
||||
const theme = getTheme(state.currentTheme);
|
||||
if (theme) {
|
||||
return theme.dark;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
chartTheme(): string {
|
||||
return this.isDarkTheme ? 'dark' : 'light';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setOpenTradesInTitle(locked: string) {
|
||||
this.openTradesInTitle = locked;
|
||||
},
|
||||
setTimeZone(timezone: string) {
|
||||
setTimezone(timezone);
|
||||
this.timezone = timezone;
|
||||
},
|
||||
setBackgroundSync(value: boolean) {
|
||||
this.backgroundSync = value;
|
||||
},
|
||||
async loadUIVersion() {
|
||||
if (import.meta.env.PROD) {
|
||||
try {
|
||||
const result = await axios.get<UiVersion>('/ui_version');
|
||||
const { version } = result.data;
|
||||
this.uiVersion = version;
|
||||
// commit('setUIVersion', version);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
key: STORE_UI_SETTINGS,
|
||||
},
|
||||
});
|
|
@ -258,4 +258,8 @@ export interface UiVersion {
|
|||
version: string;
|
||||
}
|
||||
|
||||
export type LoadingStatus = 'loading' | 'success' | 'error';
|
||||
export enum LoadingStatus {
|
||||
loading,
|
||||
success,
|
||||
error,
|
||||
}
|
||||
|
|
|
@ -2,21 +2,23 @@
|
|||
<div class="container-fluid" style="max-height: calc(100vh - 60px)">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2"></div>
|
||||
<p v-if="!canRunBacktest">Bot must be in webserver mode to enable Backtesting.</p>
|
||||
<p v-if="!botStore.activeBot.canRunBacktest">
|
||||
Bot must be in webserver mode to enable Backtesting.
|
||||
</p>
|
||||
<div class="row w-100">
|
||||
<h2 class="col-4 col-lg-3">Backtesting</h2>
|
||||
<div
|
||||
class="col-12 col-lg-order-last col-lg-6 mx-md-5 d-flex flex-wrap justify-content-md-center justify-content-between mb-4"
|
||||
:disabled="canRunBacktest"
|
||||
:disabled="botStore.activeBot.canRunBacktest"
|
||||
>
|
||||
<b-form-radio
|
||||
v-if="botApiVersion >= 2.15"
|
||||
v-if="botStore.activeBot.botApiVersion >= 2.15"
|
||||
v-model="btFormMode"
|
||||
name="bt-form-radios"
|
||||
button
|
||||
class="mx-1 flex-samesize-items"
|
||||
value="historicResults"
|
||||
:disabled="!canRunBacktest"
|
||||
:disabled="!botStore.activeBot.canRunBacktest"
|
||||
>Load Results</b-form-radio
|
||||
>
|
||||
<b-form-radio
|
||||
|
@ -25,7 +27,7 @@
|
|||
button
|
||||
class="mx-1 flex-samesize-items"
|
||||
value="run"
|
||||
:disabled="!canRunBacktest"
|
||||
:disabled="!botStore.activeBot.canRunBacktest"
|
||||
>Run backtest</b-form-radio
|
||||
>
|
||||
<b-form-radio
|
||||
|
@ -57,8 +59,11 @@
|
|||
>Visualize result</b-form-radio
|
||||
>
|
||||
</div>
|
||||
<small v-show="backtestRunning" class="text-right bt-running-label col-8 col-lg-3"
|
||||
>Backtest running: {{ backtestStep }} {{ formatPercent(backtestProgress, 2) }}</small
|
||||
<small
|
||||
v-show="botStore.activeBot.backtestRunning"
|
||||
class="text-right bt-running-label col-8 col-lg-3"
|
||||
>Backtest running: {{ botStore.activeBot.backtestStep }}
|
||||
{{ formatPercent(botStore.activeBot.backtestProgress, 2) }}</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -79,8 +84,8 @@
|
|||
<transition name="fade" mode="in-out">
|
||||
<BacktestResultSelect
|
||||
v-if="btFormMode !== 'visualize' && showLeftBar"
|
||||
:backtest-history="backtestHistory"
|
||||
:selected-backtest-result-key="selectedBacktestResultKey"
|
||||
:backtest-history="botStore.activeBot.backtestHistory"
|
||||
:selected-backtest-result-key="botStore.activeBot.selectedBacktestResultKey"
|
||||
@selectionChange="setBacktestResult"
|
||||
/>
|
||||
</transition>
|
||||
|
@ -97,7 +102,7 @@
|
|||
<span>Strategy</span>
|
||||
<StrategySelect v-model="strategy"></StrategySelect>
|
||||
</div>
|
||||
<b-card bg-variant="light" :disabled="backtestRunning">
|
||||
<b-card bg-variant="light" :disabled="botStore.activeBot.backtestRunning">
|
||||
<!-- Backtesting parameters -->
|
||||
<b-form-group
|
||||
label-cols-lg="2"
|
||||
|
@ -211,7 +216,7 @@
|
|||
<b-button
|
||||
id="start-backtest"
|
||||
variant="primary"
|
||||
:disabled="backtestRunning || !canRunBacktest"
|
||||
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
||||
class="mx-1"
|
||||
@click="clickBacktest"
|
||||
>
|
||||
|
@ -219,31 +224,31 @@
|
|||
</b-button>
|
||||
<b-button
|
||||
variant="primary"
|
||||
:disabled="backtestRunning || !canRunBacktest"
|
||||
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
||||
class="mx-1"
|
||||
@click="pollBacktest"
|
||||
@click="botStore.activeBot.pollBacktest"
|
||||
>
|
||||
Load backtest result
|
||||
</b-button>
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="mx-1"
|
||||
:disabled="!backtestRunning"
|
||||
@click="stopBacktest"
|
||||
:disabled="!botStore.activeBot.backtestRunning"
|
||||
@click="botStore.activeBot.stopBacktest"
|
||||
>Stop Backtest</b-button
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
class="mx-1"
|
||||
:disabled="backtestRunning || !canRunBacktest"
|
||||
@click="removeBacktest"
|
||||
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
||||
@click="botStore.activeBot.removeBacktest"
|
||||
>Reset Backtest</b-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<BacktestResultView
|
||||
v-if="hasBacktestResult && btFormMode == 'results'"
|
||||
:backtest-result="selectedBacktestResult"
|
||||
:backtest-result="botStore.activeBot.selectedBacktestResult"
|
||||
class="flex-fill"
|
||||
/>
|
||||
|
||||
|
@ -251,9 +256,12 @@
|
|||
v-if="hasBacktestResult && btFormMode == 'visualize-summary'"
|
||||
class="text-center flex-fill mt-2 d-flex flex-column"
|
||||
>
|
||||
<TradesLogChart :trades="selectedBacktestResult.trades" class="trades-log" />
|
||||
<TradesLogChart
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
class="trades-log"
|
||||
/>
|
||||
<CumProfitChart
|
||||
:trades="selectedBacktestResult.trades"
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
profit-column="profit_abs"
|
||||
class="cum-profit"
|
||||
:show-title="true"
|
||||
|
@ -272,19 +280,19 @@
|
|||
<PairSummary
|
||||
class="col-md-2 overflow-auto"
|
||||
style="max-height: calc(100vh - 200px)"
|
||||
:pairlist="selectedBacktestResult.pairlist"
|
||||
:trades="selectedBacktestResult.trades"
|
||||
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
sort-method="profit"
|
||||
:backtest-mode="true"
|
||||
/>
|
||||
<CandleChartContainer
|
||||
:available-pairs="selectedBacktestResult.pairlist"
|
||||
:available-pairs="botStore.activeBot.selectedBacktestResult.pairlist"
|
||||
:historic-view="!!true"
|
||||
:timeframe="timeframe"
|
||||
:plot-config="selectedPlotConfig"
|
||||
:timerange="timerange"
|
||||
:strategy="strategy"
|
||||
:trades="selectedBacktestResult.trades"
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
class="col-md-10 candle-chart-container px-0 w-100 h-100"
|
||||
>
|
||||
</CandleChartContainer>
|
||||
|
@ -292,9 +300,9 @@
|
|||
<b-card header="Single trades" class="row mt-2 w-100">
|
||||
<TradeList
|
||||
class="row trade-history mt-2 w-100"
|
||||
:trades="selectedBacktestResult.trades"
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
:show-filter="true"
|
||||
:stake-currency="selectedBacktestResult.stake_currency"
|
||||
:stake-currency="botStore.activeBot.selectedBacktestResult.stake_currency"
|
||||
/>
|
||||
</b-card>
|
||||
</div>
|
||||
|
@ -302,14 +310,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import TimeRangeSelect from '@/components/ftbot/TimeRangeSelect.vue';
|
||||
import BacktestResultView from '@/components/ftbot/BacktestResultView.vue';
|
||||
import BacktestResultSelect from '@/components/ftbot/BacktestResultSelect.vue';
|
||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||
import StrategySelect from '@/components/ftbot/StrategySelect.vue';
|
||||
import ValuePair from '@/components/general/ValuePair.vue';
|
||||
import CumProfitChart from '@/components/charts/CumProfitChart.vue';
|
||||
import TradesLogChart from '@/components/charts/TradesLog.vue';
|
||||
import PairSummary from '@/components/ftbot/PairSummary.vue';
|
||||
|
@ -317,23 +322,15 @@ import TimeframeSelect from '@/components/ftbot/TimeframeSelect.vue';
|
|||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
import BacktestHistoryLoad from '@/components/ftbot/BacktestHistoryLoad.vue';
|
||||
|
||||
import {
|
||||
BacktestPayload,
|
||||
BacktestSteps,
|
||||
BotState,
|
||||
PairHistory,
|
||||
PairHistoryPayload,
|
||||
PlotConfig,
|
||||
StrategyBacktestResult,
|
||||
} from '@/types';
|
||||
import { BacktestPayload, PlotConfig } from '@/types';
|
||||
|
||||
import { getCustomPlotConfig, getPlotConfigName } from '@/shared/storage';
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, computed, ref, onMounted, watch } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Backtesting',
|
||||
components: {
|
||||
BacktestResultView,
|
||||
BacktestResultSelect,
|
||||
|
@ -343,154 +340,125 @@ const ftbot = namespace(StoreModules.ftbot);
|
|||
CumProfitChart,
|
||||
TradesLogChart,
|
||||
StrategySelect,
|
||||
ValuePair,
|
||||
PairSummary,
|
||||
TimeframeSelect,
|
||||
TradeList,
|
||||
},
|
||||
})
|
||||
export default class Backtesting extends Vue {
|
||||
pollInterval: number | null = null;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
showLeftBar = false;
|
||||
|
||||
selectedTimeframe = '';
|
||||
|
||||
selectedDetailTimeframe = '';
|
||||
|
||||
strategy = '';
|
||||
|
||||
timerange = '';
|
||||
|
||||
enableProtections = false;
|
||||
|
||||
maxOpenTrades = '';
|
||||
|
||||
stakeAmountUnlimited = false;
|
||||
|
||||
stakeAmount = '';
|
||||
|
||||
startingCapital = '';
|
||||
|
||||
btFormMode = 'run';
|
||||
|
||||
selectedPlotConfig: PlotConfig = getCustomPlotConfig(getPlotConfigName());
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.backtestRunning]!: boolean;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.backtestStep]!: BacktestSteps;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botState]?: BotState;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botApiVersion]: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.backtestProgress]!: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.backtestHistory]!: StrategyBacktestResult[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.selectedBacktestResultKey]!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.history]!: PairHistory;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.selectedBacktestResult]!: StrategyBacktestResult;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.canRunBacktest]!: boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action getPairHistory!: (payload: PairHistoryPayload) => void;
|
||||
|
||||
@ftbot.Action getState;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action startBacktest!: (payload: BacktestPayload) => void;
|
||||
|
||||
@ftbot.Action pollBacktest!: () => void;
|
||||
|
||||
@ftbot.Action removeBacktest!: () => void;
|
||||
|
||||
@ftbot.Action stopBacktest!: () => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action setBacktestResultKey!: (key: string) => void;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
get hasBacktestResult() {
|
||||
return this.backtestHistory ? Object.keys(this.backtestHistory).length !== 0 : false;
|
||||
}
|
||||
|
||||
get timeframe(): string {
|
||||
try {
|
||||
return this.selectedBacktestResult.timeframe;
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.getState();
|
||||
}
|
||||
|
||||
setBacktestResult(key: string) {
|
||||
this.setBacktestResultKey(key);
|
||||
|
||||
// Set parameters for this result
|
||||
this.strategy = this.selectedBacktestResult.strategy_name;
|
||||
this.selectedTimeframe = this.selectedBacktestResult.timeframe;
|
||||
this.selectedDetailTimeframe = this.selectedBacktestResult.timeframe_detail || '';
|
||||
this.timerange = this.selectedBacktestResult.timerange;
|
||||
}
|
||||
|
||||
clickBacktest() {
|
||||
const btPayload: BacktestPayload = {
|
||||
strategy: this.strategy,
|
||||
timerange: this.timerange,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
enable_protections: this.enableProtections,
|
||||
};
|
||||
const openTradesInt = parseInt(this.maxOpenTrades, 10);
|
||||
if (openTradesInt) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.max_open_trades = openTradesInt;
|
||||
}
|
||||
if (this.stakeAmountUnlimited) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.stake_amount = 'unlimited';
|
||||
} else {
|
||||
const stakeAmount = Number(this.stakeAmount);
|
||||
if (stakeAmount) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.stake_amount = stakeAmount.toString();
|
||||
const hasBacktestResult = computed(() =>
|
||||
botStore.activeBot.backtestHistory
|
||||
? Object.keys(botStore.activeBot.backtestHistory).length !== 0
|
||||
: false,
|
||||
);
|
||||
const timeframe = computed((): string => {
|
||||
try {
|
||||
return botStore.activeBot.selectedBacktestResult.timeframe;
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const startingCapital = Number(this.startingCapital);
|
||||
if (startingCapital) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.dry_run_wallet = startingCapital;
|
||||
}
|
||||
const strategy = ref('');
|
||||
const selectedTimeframe = ref('');
|
||||
const selectedDetailTimeframe = ref('');
|
||||
const timerange = ref('');
|
||||
const showLeftBar = ref(false);
|
||||
const enableProtections = ref(false);
|
||||
const stakeAmountUnlimited = ref(false);
|
||||
const maxOpenTrades = ref('');
|
||||
const stakeAmount = ref('');
|
||||
const startingCapital = ref('');
|
||||
const btFormMode = ref('run');
|
||||
const pollInterval = ref<number | null>(null);
|
||||
const selectedPlotConfig = ref<PlotConfig>(getCustomPlotConfig(getPlotConfigName()));
|
||||
|
||||
if (this.selectedTimeframe) {
|
||||
btPayload.timeframe = this.selectedTimeframe;
|
||||
}
|
||||
if (this.selectedDetailTimeframe) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.timeframe_detail = this.selectedDetailTimeframe;
|
||||
}
|
||||
const setBacktestResult = (key: string) => {
|
||||
botStore.activeBot.setBacktestResultKey(key);
|
||||
|
||||
this.startBacktest(btPayload);
|
||||
}
|
||||
// Set parameters for this result
|
||||
strategy.value = botStore.activeBot.selectedBacktestResult.strategy_name;
|
||||
selectedTimeframe.value = botStore.activeBot.selectedBacktestResult.timeframe;
|
||||
selectedDetailTimeframe.value =
|
||||
botStore.activeBot.selectedBacktestResult.timeframe_detail || '';
|
||||
timerange.value = botStore.activeBot.selectedBacktestResult.timerange;
|
||||
};
|
||||
|
||||
@Watch('backtestRunning')
|
||||
backtestRunningChanged() {
|
||||
if (this.backtestRunning === true) {
|
||||
this.pollInterval = window.setInterval(this.pollBacktest, 1000);
|
||||
} else if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
const clickBacktest = () => {
|
||||
const btPayload: BacktestPayload = {
|
||||
strategy: strategy.value,
|
||||
timerange: timerange.value,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
enable_protections: enableProtections.value,
|
||||
};
|
||||
const openTradesInt = parseInt(maxOpenTrades.value, 10);
|
||||
if (openTradesInt) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.max_open_trades = openTradesInt;
|
||||
}
|
||||
if (stakeAmountUnlimited.value) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.stake_amount = 'unlimited';
|
||||
} else {
|
||||
const stakeAmountLoc = Number(stakeAmount.value);
|
||||
if (stakeAmountLoc) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.stake_amount = stakeAmountLoc.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const startingCapitalLoc = Number(startingCapital.value);
|
||||
if (startingCapitalLoc) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.dry_run_wallet = startingCapitalLoc;
|
||||
}
|
||||
|
||||
if (selectedTimeframe.value) {
|
||||
btPayload.timeframe = selectedTimeframe.value;
|
||||
}
|
||||
if (selectedDetailTimeframe.value) {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
btPayload.timeframe_detail = selectedDetailTimeframe.value;
|
||||
}
|
||||
|
||||
botStore.activeBot.startBacktest(btPayload);
|
||||
};
|
||||
onMounted(() => botStore.activeBot.getState());
|
||||
watch(
|
||||
() => botStore.activeBot.backtestRunning,
|
||||
() => {
|
||||
if (botStore.activeBot.backtestRunning === true) {
|
||||
pollInterval.value = window.setInterval(botStore.activeBot.pollBacktest, 1000);
|
||||
} else if (pollInterval.value) {
|
||||
clearInterval(pollInterval.value);
|
||||
pollInterval.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
botStore,
|
||||
|
||||
formatPercent,
|
||||
hasBacktestResult,
|
||||
timeframe,
|
||||
setBacktestResult,
|
||||
strategy,
|
||||
selectedTimeframe,
|
||||
selectedDetailTimeframe,
|
||||
timerange,
|
||||
enableProtections,
|
||||
showLeftBar,
|
||||
stakeAmountUnlimited,
|
||||
maxOpenTrades,
|
||||
stakeAmount,
|
||||
startingCapital,
|
||||
btFormMode,
|
||||
selectedPlotConfig,
|
||||
clickBacktest,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<GridLayout
|
||||
class="h-100 w-100"
|
||||
:row-height="50"
|
||||
:layout.sync="gridLayout"
|
||||
:layout="gridLayout"
|
||||
:vertical-compact="false"
|
||||
:margin="[5, 5]"
|
||||
:responsive-layouts="responsiveGridLayouts"
|
||||
|
@ -11,7 +11,7 @@
|
|||
:responsive="true"
|
||||
:prevent-collision="true"
|
||||
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||
@layout-updated="layoutUpdated"
|
||||
@layout-updated="layoutUpdatedEvent"
|
||||
@breakpoint-changed="breakpointChanged"
|
||||
>
|
||||
<GridItem
|
||||
|
@ -24,10 +24,10 @@
|
|||
:min-h="4"
|
||||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer :header="`Daily Profit ${botCount > 1 ? 'combined' : ''}`">
|
||||
<DraggableContainer :header="`Daily Profit ${botStore.botCount > 1 ? 'combined' : ''}`">
|
||||
<DailyChart
|
||||
v-if="allDailyStatsAllBots"
|
||||
:daily-stats="allDailyStatsAllBots"
|
||||
v-if="botStore.allDailyStatsAllBots"
|
||||
:daily-stats="botStore.allDailyStatsAllBots"
|
||||
:show-title="false"
|
||||
/>
|
||||
</DraggableContainer>
|
||||
|
@ -57,7 +57,7 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Open Trades">
|
||||
<trade-list :active-trades="true" :trades="allOpenTradesAllBots" multi-bot-view />
|
||||
<trade-list :active-trades="true" :trades="botStore.allOpenTradesAllBots" multi-bot-view />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
|
@ -71,7 +71,7 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Cumulative Profit">
|
||||
<CumProfitChart :trades="allTradesAllBots" :show-title="false" />
|
||||
<CumProfitChart :trades="botStore.allTradesAllBots" :show-title="false" />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
|
@ -85,7 +85,7 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Trades Log">
|
||||
<TradesLogChart :trades="allTradesAllBots" :show-title="false" />
|
||||
<TradesLogChart :trades="botStore.allTradesAllBots" :show-title="false" />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
</GridLayout>
|
||||
|
@ -94,8 +94,6 @@
|
|||
<script lang="ts">
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
|
||||
|
||||
import DailyChart from '@/components/charts/DailyChart.vue';
|
||||
|
@ -105,21 +103,12 @@ import BotComparisonList from '@/components/ftbot/BotComparisonList.vue';
|
|||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
||||
|
||||
import {
|
||||
DashboardLayout,
|
||||
findGridLayout,
|
||||
LayoutActions,
|
||||
LayoutGetters,
|
||||
} from '@/store/modules/layout';
|
||||
import { Trade, DailyReturnValue, DailyPayload, ClosedTrade } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, ref, computed, onMounted } from '@vue/composition-api';
|
||||
import { DashboardLayout, findGridLayout, useLayoutStore } from '@/stores/layout';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
const layoutNs = namespace(StoreModules.layout);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
GridLayout,
|
||||
GridItem,
|
||||
|
@ -130,111 +119,87 @@ const layoutNs = namespace(StoreModules.layout);
|
|||
TradeList,
|
||||
DraggableContainer,
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
@ftbot.Getter [MultiBotStoreGetters.botCount]!: number;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allOpenTradesAllBots]!: Trade[];
|
||||
const layoutStore = useLayoutStore();
|
||||
const currentBreakpoint = ref('');
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allTradesAllBots]!: ClosedTrade[];
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allDailyStatsAllBots]!: Record<string, DailyReturnValue>;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.performanceStats]!: PerformanceEntry[];
|
||||
|
||||
@ftbot.Action getPerformance;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action allGetDaily!: (payload?: DailyPayload) => void;
|
||||
|
||||
@ftbot.Action getTrades;
|
||||
|
||||
@ftbot.Action getOpenTrades;
|
||||
|
||||
@ftbot.Action getProfit;
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getDashboardLayoutSm]!: GridItemData[];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getDashboardLayout]!: GridItemData[];
|
||||
|
||||
@layoutNs.Action [LayoutActions.setDashboardLayout];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
localGridLayout: GridItemData[] = [];
|
||||
|
||||
currentBreakpoint = '';
|
||||
|
||||
get isLayoutLocked() {
|
||||
return this.getLayoutLocked || !this.isResizableLayout;
|
||||
}
|
||||
|
||||
get isResizableLayout() {
|
||||
return ['', 'sm', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
||||
}
|
||||
|
||||
get gridLayout() {
|
||||
if (this.isResizableLayout) {
|
||||
return this.getDashboardLayout;
|
||||
}
|
||||
return this.localGridLayout;
|
||||
}
|
||||
|
||||
set gridLayout(newLayout) {
|
||||
// Dummy setter to make gridLayout happy. Updates happen through layoutUpdated.
|
||||
}
|
||||
|
||||
layoutUpdated(newLayout) {
|
||||
// Frozen layouts for small screen sizes.
|
||||
if (this.isResizableLayout) {
|
||||
console.log('newlayout', newLayout);
|
||||
console.log('saving dashboard');
|
||||
this.setDashboardLayout(newLayout);
|
||||
}
|
||||
}
|
||||
|
||||
get gridLayoutDaily(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.dailyChart);
|
||||
}
|
||||
|
||||
get gridLayoutBotComparison(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.botComparison);
|
||||
}
|
||||
|
||||
get gridLayoutAllOpenTrades(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.allOpenTrades);
|
||||
}
|
||||
|
||||
get gridLayoutCumChart(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.cumChartChart);
|
||||
}
|
||||
|
||||
get gridLayoutTradesLogChart(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.tradesLogChart);
|
||||
}
|
||||
|
||||
get responsiveGridLayouts() {
|
||||
return {
|
||||
sm: this.getDashboardLayoutSm,
|
||||
const breakpointChanged = (newBreakpoint) => {
|
||||
// // console.log('breakpoint:', newBreakpoint);
|
||||
currentBreakpoint.value = newBreakpoint;
|
||||
};
|
||||
}
|
||||
const isResizableLayout = computed(() =>
|
||||
['', 'sm', 'md', 'lg', 'xl'].includes(currentBreakpoint.value),
|
||||
);
|
||||
const isLayoutLocked = computed(() => {
|
||||
return layoutStore.layoutLocked || !isResizableLayout;
|
||||
});
|
||||
|
||||
mounted() {
|
||||
this.allGetDaily({ timescale: 30 });
|
||||
this.getTrades();
|
||||
this.getOpenTrades();
|
||||
this.getPerformance();
|
||||
this.getProfit();
|
||||
this.localGridLayout = [...this.getDashboardLayoutSm];
|
||||
}
|
||||
const gridLayout = computed((): GridItemData[] => {
|
||||
if (isResizableLayout) {
|
||||
return layoutStore.dashboardLayout;
|
||||
}
|
||||
return [...layoutStore.getDashboardLayoutSm];
|
||||
});
|
||||
|
||||
breakpointChanged(newBreakpoint) {
|
||||
// console.log('breakpoint:', newBreakpoint);
|
||||
this.currentBreakpoint = newBreakpoint;
|
||||
}
|
||||
}
|
||||
const layoutUpdatedEvent = (newLayout) => {
|
||||
if (isResizableLayout) {
|
||||
console.log('newlayout', newLayout);
|
||||
console.log('saving dashboard');
|
||||
layoutStore.dashboardLayout = newLayout;
|
||||
}
|
||||
};
|
||||
|
||||
const gridLayoutDaily = computed((): GridItemData => {
|
||||
return findGridLayout(gridLayout.value, DashboardLayout.dailyChart);
|
||||
});
|
||||
|
||||
const gridLayoutBotComparison = computed((): GridItemData => {
|
||||
return findGridLayout(gridLayout.value, DashboardLayout.botComparison);
|
||||
});
|
||||
|
||||
const gridLayoutAllOpenTrades = computed((): GridItemData => {
|
||||
return findGridLayout(gridLayout.value, DashboardLayout.allOpenTrades);
|
||||
});
|
||||
|
||||
const gridLayoutCumChart = computed((): GridItemData => {
|
||||
return findGridLayout(gridLayout.value, DashboardLayout.cumChartChart);
|
||||
});
|
||||
|
||||
const gridLayoutTradesLogChart = computed((): GridItemData => {
|
||||
return findGridLayout(gridLayout.value, DashboardLayout.tradesLogChart);
|
||||
});
|
||||
|
||||
const responsiveGridLayouts = computed(() => {
|
||||
return {
|
||||
sm: layoutStore.getDashboardLayoutSm,
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await botStore.allGetDaily({ timescale: 30 });
|
||||
botStore.activeBot.getTrades();
|
||||
botStore.activeBot.getOpenTrades();
|
||||
botStore.activeBot.getProfit();
|
||||
});
|
||||
|
||||
return {
|
||||
botStore,
|
||||
formatPrice,
|
||||
isLayoutLocked,
|
||||
layoutUpdatedEvent,
|
||||
breakpointChanged,
|
||||
gridLayout,
|
||||
gridLayoutDaily,
|
||||
gridLayoutBotComparison,
|
||||
gridLayoutAllOpenTrades,
|
||||
gridLayoutCumChart,
|
||||
gridLayoutTradesLogChart,
|
||||
responsiveGridLayouts,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -10,9 +10,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
export default Vue.extend({});
|
||||
export default defineComponent({
|
||||
name: 'Error404',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<!-- Currently only available in Webserver mode -->
|
||||
<!-- <b-checkbox v-model="historicView">HistoricData</b-checkbox> -->
|
||||
<!-- </div> -->
|
||||
<div v-if="historicView" class="mx-md-3 mt-2">
|
||||
<div v-if="botStore.activeBot.isWebserverMode" class="mx-md-3 mt-2">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div class="col-md-3 text-left">
|
||||
<span>Strategy</span>
|
||||
|
@ -20,12 +20,18 @@
|
|||
|
||||
<div class="mx-2 mt-2 pb-1 h-100">
|
||||
<CandleChartContainer
|
||||
:available-pairs="historicView ? pairlist : whitelist"
|
||||
:historic-view="historicView"
|
||||
:timeframe="historicView ? selectedTimeframe : timeframe"
|
||||
:trades="trades"
|
||||
:timerange="historicView ? timerange : ''"
|
||||
:strategy="historicView ? strategy : ''"
|
||||
:available-pairs="
|
||||
botStore.activeBot.isWebserverMode
|
||||
? botStore.activeBot.pairlist
|
||||
: botStore.activeBot.whitelist
|
||||
"
|
||||
:historic-view="botStore.activeBot.isWebserverMode"
|
||||
:timeframe="
|
||||
botStore.activeBot.isWebserverMode ? selectedTimeframe : botStore.activeBot.timeframe
|
||||
"
|
||||
:trades="botStore.activeBot.trades"
|
||||
:timerange="botStore.activeBot.isWebserverMode ? timerange : ''"
|
||||
:strategy="botStore.activeBot.isWebserverMode ? strategy : ''"
|
||||
:plot-config-modal="false"
|
||||
>
|
||||
</CandleChartContainer>
|
||||
|
@ -34,61 +40,42 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||
import TimeRangeSelect from '@/components/ftbot/TimeRangeSelect.vue';
|
||||
import TimeframeSelect from '@/components/ftbot/TimeframeSelect.vue';
|
||||
import StrategySelect from '@/components/ftbot/StrategySelect.vue';
|
||||
import { AvailablePairPayload, AvailablePairResult, Trade, WhitelistResponse } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, onMounted, ref } from '@vue/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Graphs',
|
||||
components: { CandleChartContainer, StrategySelect, TimeRangeSelect, TimeframeSelect },
|
||||
})
|
||||
export default class Graphs extends Vue {
|
||||
historicView = false;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const strategy = ref('');
|
||||
const timerange = ref('');
|
||||
const selectedTimeframe = ref('');
|
||||
|
||||
strategy = '';
|
||||
onMounted(() => {
|
||||
if (botStore.activeBot.isWebserverMode) {
|
||||
// this.refresh();
|
||||
botStore.activeBot.getAvailablePairs({ timeframe: botStore.activeBot.timeframe });
|
||||
// .then((val) => {
|
||||
// console.log(val);
|
||||
// });
|
||||
} else if (!botStore.activeBot.whitelist || botStore.activeBot.whitelist.length === 0) {
|
||||
botStore.activeBot.getWhitelist();
|
||||
}
|
||||
});
|
||||
|
||||
timerange = '';
|
||||
|
||||
selectedTimeframe = '';
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.pairlist]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.whitelist]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.trades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.timeframe]!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.isWebserverMode]!: boolean;
|
||||
|
||||
@ftbot.Action public getWhitelist!: () => Promise<WhitelistResponse>;
|
||||
|
||||
@ftbot.Action public getAvailablePairs!: (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
payload: AvailablePairPayload,
|
||||
) => Promise<AvailablePairResult>;
|
||||
|
||||
mounted() {
|
||||
this.historicView = this.isWebserverMode;
|
||||
if (!this.whitelist || this.whitelist.length === 0) {
|
||||
this.getWhitelist();
|
||||
}
|
||||
if (this.historicView) {
|
||||
// this.refresh();
|
||||
this.getAvailablePairs({ timeframe: this.timeframe });
|
||||
// .then((val) => {
|
||||
// console.log(val);
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
strategy,
|
||||
timerange,
|
||||
selectedTimeframe,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -21,14 +21,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
import BotList from '@/components/BotList.vue';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Home',
|
||||
components: { BotList },
|
||||
})
|
||||
export default class Home extends Vue {}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import LogViewer from '@/components/ftbot/LogViewer.vue';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'LogView',
|
||||
components: { LogViewer },
|
||||
})
|
||||
export default class LogView extends Vue {}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,43 +1,43 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-button v-b-modal.modal-prevent-closing>{{ loginText }}</b-button>
|
||||
<b-modal id="modal-prevent-closing" ref="modal" title="Login to your bot" @ok="handleOk">
|
||||
<b-modal id="modal-prevent-closing" ref="modalRef" title="Login to your bot" @ok="handleOk">
|
||||
<Login id="loginForm" ref="loginForm" in-modal @loginResult="handleLoginResult" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
|
||||
import Login from '@/components/Login.vue';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'LoginModal',
|
||||
components: { Login },
|
||||
})
|
||||
export default class LoginModal extends Vue {
|
||||
$refs!: {
|
||||
loginForm: HTMLFormElement;
|
||||
modal: HTMLElement;
|
||||
};
|
||||
|
||||
@Prop({ required: false, default: 'Login', type: String }) loginText!: string;
|
||||
|
||||
resetLogin() {
|
||||
// this.$refs.loginForm.resetLogin();
|
||||
}
|
||||
|
||||
handleLoginResult(result: boolean) {
|
||||
if (result) {
|
||||
(this.$refs.modal as any).hide();
|
||||
}
|
||||
}
|
||||
|
||||
handleOk(evt) {
|
||||
evt.preventDefault();
|
||||
this.$refs.loginForm.handleSubmit();
|
||||
}
|
||||
}
|
||||
props: {
|
||||
loginText: { required: false, default: 'Login', type: String },
|
||||
},
|
||||
setup() {
|
||||
const modalRef = ref<HTMLElement>();
|
||||
const loginForm = ref<HTMLFormElement>();
|
||||
const handleLoginResult = (result: boolean) => {
|
||||
if (result) {
|
||||
(modalRef.value as any)?.hide();
|
||||
}
|
||||
};
|
||||
const handleOk = (evt) => {
|
||||
evt.preventDefault();
|
||||
loginForm.value?.handleSubmit();
|
||||
};
|
||||
return {
|
||||
modalRef,
|
||||
loginForm,
|
||||
handleOk,
|
||||
handleLoginResult,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
||||
import Login from '@/components/Login.vue';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'LoginView',
|
||||
components: { Login },
|
||||
})
|
||||
export default class LoginView extends Vue {}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="container mt-3">
|
||||
<b-card header="FreqUI Settings">
|
||||
<div class="text-left">
|
||||
<p>UI Version: {{ getUiVersion }}</p>
|
||||
<p>UI Version: {{ settingsStore.uiVersion }}</p>
|
||||
<b-form-group
|
||||
description="Lock dynamic layouts, so they cannot move anymore. Can also be set from the navbar at the top."
|
||||
>
|
||||
<b-checkbox v-model="layoutLockedLocal">Lock layout</b-checkbox>
|
||||
<b-checkbox v-model="layoutStore.layoutLocked">Lock layout</b-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group description="Reset dynamic layouts to how they were.">
|
||||
<b-button size="sm" @click="resetDynamicLayout">Reset layout</b-button>
|
||||
|
@ -16,7 +16,7 @@
|
|||
description="Decide if open trades should be visualized"
|
||||
>
|
||||
<b-form-select
|
||||
v-model="openTradesVisualization"
|
||||
v-model="settingsStore.openTradesInTitle"
|
||||
:options="openTradesOptions"
|
||||
></b-form-select>
|
||||
</b-form-group>
|
||||
|
@ -24,10 +24,13 @@
|
|||
label="UTC Timezone"
|
||||
description="Select timezone (we recommend UTC is recommended as exchanges usually work in UTC)"
|
||||
>
|
||||
<b-form-select v-model="timezoneLoc" :options="timezoneOptions"></b-form-select>
|
||||
<b-form-select
|
||||
v-model="settingsStore.timezone"
|
||||
:options="timezoneOptions"
|
||||
></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group description="Keep background sync running while other bots are selected.">
|
||||
<b-checkbox v-model="backgroundSyncLocal">Background sync</b-checkbox>
|
||||
<b-checkbox v-model="settingsStore.backgroundSync">Background sync</b-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card>
|
||||
|
@ -35,90 +38,39 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AlertActions } from '@/store/modules/alerts';
|
||||
import { LayoutActions, LayoutGetters } from '@/store/modules/layout';
|
||||
import { OpenTradeVizOptions, SettingsActions, SettingsGetters } from '@/store/modules/settings';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace, Getter } from 'vuex-class';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { OpenTradeVizOptions, useSettingsStore } from '@/stores/settings';
|
||||
import { useLayoutStore } from '@/stores/layout';
|
||||
import { showAlert } from '@/stores/alerts';
|
||||
|
||||
const layoutNs = namespace(StoreModules.layout);
|
||||
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
||||
const alerts = namespace(StoreModules.alerts);
|
||||
export default defineComponent({
|
||||
name: 'Settings',
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
@Component({})
|
||||
export default class Settings extends Vue {
|
||||
@Getter getUiVersion!: string;
|
||||
const timezoneOptions = ['UTC', Intl.DateTimeFormat().resolvedOptions().timeZone];
|
||||
const openTradesOptions = [
|
||||
{ value: OpenTradeVizOptions.showPill, text: 'Show pill in icon' },
|
||||
{ value: OpenTradeVizOptions.asTitle, text: 'Show in title' },
|
||||
{ value: OpenTradeVizOptions.noOpenTrades, text: "Don't show open trades in header" },
|
||||
];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||
|
||||
@layoutNs.Action [LayoutActions.setLayoutLocked];
|
||||
|
||||
@layoutNs.Action [LayoutActions.resetTradingLayout];
|
||||
|
||||
@layoutNs.Action [LayoutActions.resetDashboardLayout];
|
||||
|
||||
@alerts.Action [AlertActions.addAlert];
|
||||
|
||||
@uiSettingsNs.Getter [SettingsGetters.openTradesInTitle]: string;
|
||||
|
||||
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
||||
|
||||
@uiSettingsNs.Getter [SettingsGetters.backgroundSync]: boolean;
|
||||
|
||||
@uiSettingsNs.Action [SettingsActions.setOpenTradesInTitle];
|
||||
|
||||
@uiSettingsNs.Action [SettingsActions.setTimeZone];
|
||||
|
||||
@uiSettingsNs.Action [SettingsActions.setBackgroundSync];
|
||||
|
||||
openTradesOptions = [
|
||||
{ value: OpenTradeVizOptions.showPill, text: 'Show pill in icon' },
|
||||
{ value: OpenTradeVizOptions.asTitle, text: 'Show in title' },
|
||||
{ value: OpenTradeVizOptions.noOpenTrades, text: "Don't show open trades in header" },
|
||||
];
|
||||
|
||||
// Careful when adding new timezones here - eCharts only supports UTC or user timezone
|
||||
timezoneOptions = ['UTC', Intl.DateTimeFormat().resolvedOptions().timeZone];
|
||||
|
||||
get timezoneLoc() {
|
||||
return this.timezone;
|
||||
}
|
||||
|
||||
set timezoneLoc(value: string) {
|
||||
this[SettingsActions.setTimeZone](value);
|
||||
}
|
||||
|
||||
get openTradesVisualization() {
|
||||
return this.openTradesInTitle;
|
||||
}
|
||||
|
||||
set openTradesVisualization(value: string) {
|
||||
this.setOpenTradesInTitle(value);
|
||||
}
|
||||
|
||||
get layoutLockedLocal() {
|
||||
return this.getLayoutLocked;
|
||||
}
|
||||
|
||||
set layoutLockedLocal(value: boolean) {
|
||||
this.setLayoutLocked(value);
|
||||
}
|
||||
|
||||
get backgroundSyncLocal(): boolean {
|
||||
return this.backgroundSync;
|
||||
}
|
||||
|
||||
set backgroundSyncLocal(value: boolean) {
|
||||
this.setBackgroundSync(value);
|
||||
}
|
||||
|
||||
resetDynamicLayout(): void {
|
||||
this.resetTradingLayout();
|
||||
this.resetDashboardLayout();
|
||||
this.addAlert({ message: 'Layouts have been reset.' });
|
||||
}
|
||||
}
|
||||
//
|
||||
const resetDynamicLayout = () => {
|
||||
layoutStore.resetTradingLayout();
|
||||
layoutStore.resetDashboardLayout();
|
||||
showAlert('Layouts have been reset.');
|
||||
};
|
||||
return {
|
||||
resetDynamicLayout,
|
||||
settingsStore,
|
||||
layoutStore,
|
||||
timezoneOptions,
|
||||
openTradesOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -8,66 +8,60 @@
|
|||
empty-text="Currently no open trades."
|
||||
/> -->
|
||||
<CustomTradeList
|
||||
v-if="!history && !detailTradeId"
|
||||
:trades="openTrades"
|
||||
v-if="!history && !botStore.activeBot.detailTradeId"
|
||||
:trades="botStore.activeBot.openTrades"
|
||||
title="Open trades"
|
||||
:active-trades="true"
|
||||
:stake-currency-decimals="stakeCurrencyDecimals"
|
||||
:stake-currency-decimals="botStore.activeBot.stakeCurrencyDecimals"
|
||||
empty-text="No open Trades."
|
||||
/>
|
||||
<CustomTradeList
|
||||
v-if="history && !detailTradeId"
|
||||
:trades="closedTrades"
|
||||
v-if="history && !botStore.activeBot.detailTradeId"
|
||||
:trades="botStore.activeBot.closedTrades"
|
||||
title="Trade history"
|
||||
:stake-currency-decimals="stakeCurrencyDecimals"
|
||||
:stake-currency-decimals="botStore.activeBot.stakeCurrencyDecimals"
|
||||
empty-text="No closed trades so far."
|
||||
/>
|
||||
<div v-if="detailTradeId" class="d-flex flex-column">
|
||||
<b-button size="sm" class="align-self-start mt-1 ml-1" @click="setDetailTrade(null)"
|
||||
<div v-if="botStore.activeBot.detailTradeId" class="d-flex flex-column">
|
||||
<b-button
|
||||
size="sm"
|
||||
class="align-self-start mt-1 ml-1"
|
||||
@click="botStore.activeBot.setDetailTrade(null)"
|
||||
><BackIcon /> Back</b-button
|
||||
>
|
||||
<TradeDetail :trade="tradeDetail" :stake-currency="stakeCurrency" />
|
||||
<TradeDetail
|
||||
:trade="botStore.activeBot.tradeDetail"
|
||||
:stake-currency="botStore.activeBot.stakeCurrency"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
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/composition-api';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
import { Trade } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
// TODO: TradeDetail could be extracted into a sub-route to allow direct access
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'TradesList',
|
||||
components: {
|
||||
CustomTradeList,
|
||||
TradeDetail,
|
||||
BackIcon,
|
||||
},
|
||||
})
|
||||
export default class TradesList extends Vue {
|
||||
@Prop({ default: false }) history!: boolean;
|
||||
props: {
|
||||
history: { default: false, type: Boolean },
|
||||
},
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.openTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.closedTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrencyDecimals]!: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.detailTradeId]?: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.tradeDetail]!: Trade;
|
||||
|
||||
@ftbot.Action setDetailTrade;
|
||||
}
|
||||
return {
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -25,7 +25,11 @@
|
|||
<DraggableContainer header="Multi Pane">
|
||||
<b-tabs content-class="mt-3" class="mt-1">
|
||||
<b-tab title="Pairs combined" active>
|
||||
<PairSummary :pairlist="whitelist" :current-locks="currentLocks" :trades="openTrades" />
|
||||
<PairSummary
|
||||
:pairlist="botStore.activeBot.whitelist"
|
||||
:current-locks="botStore.activeBot.activeLocks"
|
||||
:trades="botStore.activeBot.openTrades"
|
||||
/>
|
||||
</b-tab>
|
||||
<b-tab title="General">
|
||||
<div class="d-flex justify-content-center">
|
||||
|
@ -64,7 +68,7 @@
|
|||
<DraggableContainer header="Open Trades">
|
||||
<TradeList
|
||||
class="open-trades"
|
||||
:trades="openTrades"
|
||||
:trades="botStore.activeBot.openTrades"
|
||||
title="Open trades"
|
||||
:active-trades="true"
|
||||
empty-text="Currently no open trades."
|
||||
|
@ -83,7 +87,7 @@
|
|||
<DraggableContainer header="Closed Trades">
|
||||
<TradeList
|
||||
class="trade-history"
|
||||
:trades="closedTrades"
|
||||
:trades="botStore.activeBot.closedTrades"
|
||||
title="Trade history"
|
||||
:show-filter="true"
|
||||
empty-text="No closed trades so far."
|
||||
|
@ -91,7 +95,7 @@
|
|||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
v-if="detailTradeId && gridLayoutTradeDetail.h != 0"
|
||||
v-if="botStore.activeBot.detailTradeId && gridLayoutTradeDetail.h != 0"
|
||||
:i="gridLayoutTradeDetail.i"
|
||||
:x="gridLayoutTradeDetail.x"
|
||||
:y="gridLayoutTradeDetail.y"
|
||||
|
@ -101,7 +105,10 @@
|
|||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Trade Detail">
|
||||
<TradeDetail :trade="tradeDetail" :stake-currency="stakeCurrency" />
|
||||
<TradeDetail
|
||||
:trade="botStore.activeBot.tradeDetail"
|
||||
:stake-currency="botStore.activeBot.stakeCurrency"
|
||||
/>
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
|
@ -116,10 +123,10 @@
|
|||
>
|
||||
<DraggableContainer header="Chart">
|
||||
<CandleChartContainer
|
||||
:available-pairs="whitelist"
|
||||
:available-pairs="botStore.activeBot.whitelist"
|
||||
:historic-view="!!false"
|
||||
:timeframe="timeframe"
|
||||
:trades="allTrades"
|
||||
:timeframe="botStore.activeBot.timeframe"
|
||||
:trades="botStore.activeBot.allTrades"
|
||||
>
|
||||
</CandleChartContainer>
|
||||
</DraggableContainer>
|
||||
|
@ -128,8 +135,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
|
||||
|
||||
import Balance from '@/components/ftbot/Balance.vue';
|
||||
|
@ -145,15 +150,12 @@ import Performance from '@/components/ftbot/Performance.vue';
|
|||
import TradeDetail from '@/components/ftbot/TradeDetail.vue';
|
||||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
|
||||
import { Lock, Trade } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { TradeLayout, findGridLayout, LayoutGetters, LayoutActions } from '@/store/modules/layout';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import { defineComponent, ref, computed } from '@vue/composition-api';
|
||||
import { useLayoutStore, findGridLayout, TradeLayout } from '@/stores/layout';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
const layoutNs = namespace(StoreModules.layout);
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
name: 'Trading',
|
||||
components: {
|
||||
Balance,
|
||||
BotControls,
|
||||
|
@ -170,98 +172,78 @@ const layoutNs = namespace(StoreModules.layout);
|
|||
TradeDetail,
|
||||
TradeList,
|
||||
},
|
||||
})
|
||||
export default class Trading extends Vue {
|
||||
@ftbot.Getter [BotStoreGetters.detailTradeId]!: number;
|
||||
setup() {
|
||||
const botStore = useBotStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
const currentBreakpoint = ref('');
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.openTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.closedTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.allTrades]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.tradeDetail]!: Trade;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.timeframe]!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.currentLocks]!: Lock[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.whitelist]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getTradingLayout]!: GridItemData[];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getTradingLayoutSm]!: GridItemData[];
|
||||
|
||||
@layoutNs.Action [LayoutActions.setTradingLayout];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||
|
||||
currentBreakpoint = '';
|
||||
|
||||
localGridLayout: GridItemData[] = [];
|
||||
|
||||
get isLayoutLocked() {
|
||||
return this.getLayoutLocked || !this.isResizableLayout;
|
||||
}
|
||||
|
||||
get isResizableLayout() {
|
||||
return ['', 'sm', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
||||
}
|
||||
|
||||
get gridLayout(): GridItemData[] {
|
||||
if (this.isResizableLayout) {
|
||||
return this.getTradingLayout;
|
||||
}
|
||||
return this.localGridLayout;
|
||||
}
|
||||
|
||||
set gridLayout(newLayout) {
|
||||
// Dummy setter to make gridLayout happy. Updates happen through layoutUpdated.
|
||||
}
|
||||
|
||||
get gridLayoutMultiPane(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.multiPane);
|
||||
}
|
||||
|
||||
get gridLayoutOpenTrades(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.openTrades);
|
||||
}
|
||||
|
||||
get gridLayoutTradeHistory(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.tradeHistory);
|
||||
}
|
||||
|
||||
get gridLayoutTradeDetail(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.tradeDetail);
|
||||
}
|
||||
|
||||
get gridLayoutChartView(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.chartView);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.localGridLayout = [...this.getTradingLayoutSm];
|
||||
}
|
||||
|
||||
layoutUpdatedEvent(newLayout) {
|
||||
if (this.isResizableLayout) {
|
||||
this.setTradingLayout(newLayout);
|
||||
}
|
||||
}
|
||||
|
||||
get responsiveGridLayouts() {
|
||||
return {
|
||||
sm: this[LayoutGetters.getTradingLayoutSm],
|
||||
const breakpointChanged = (newBreakpoint) => {
|
||||
// console.log('breakpoint:', newBreakpoint);
|
||||
currentBreakpoint.value = newBreakpoint;
|
||||
};
|
||||
}
|
||||
const isResizableLayout = computed(() =>
|
||||
['', 'sm', 'md', 'lg', 'xl'].includes(currentBreakpoint.value),
|
||||
);
|
||||
const isLayoutLocked = computed(() => {
|
||||
return layoutStore.layoutLocked || !isResizableLayout;
|
||||
});
|
||||
const gridLayout = computed((): GridItemData[] => {
|
||||
if (isResizableLayout) {
|
||||
return layoutStore.tradingLayout;
|
||||
}
|
||||
return [...layoutStore.getTradingLayoutSm];
|
||||
});
|
||||
|
||||
breakpointChanged(newBreakpoint) {
|
||||
console.log('breakpoint:', newBreakpoint);
|
||||
this.currentBreakpoint = newBreakpoint;
|
||||
}
|
||||
}
|
||||
const gridLayoutMultiPane = computed(() => {
|
||||
return findGridLayout(gridLayout.value, TradeLayout.multiPane);
|
||||
});
|
||||
|
||||
const gridLayoutOpenTrades = computed(() => {
|
||||
return findGridLayout(gridLayout.value, TradeLayout.openTrades);
|
||||
});
|
||||
|
||||
const gridLayoutTradeHistory = computed(() => {
|
||||
return findGridLayout(gridLayout.value, TradeLayout.tradeHistory);
|
||||
});
|
||||
|
||||
const gridLayoutTradeDetail = computed(() => {
|
||||
return findGridLayout(gridLayout.value, TradeLayout.tradeDetail);
|
||||
});
|
||||
|
||||
const gridLayoutChartView = computed(() => {
|
||||
return findGridLayout(gridLayout.value, TradeLayout.chartView);
|
||||
});
|
||||
|
||||
const responsiveGridLayouts = computed(() => {
|
||||
return {
|
||||
sm: layoutStore.getTradingLayoutSm,
|
||||
};
|
||||
});
|
||||
|
||||
const layoutUpdatedEvent = (newLayout) => {
|
||||
if (isResizableLayout) {
|
||||
layoutStore.tradingLayout = newLayout;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
botStore,
|
||||
|
||||
layoutStore,
|
||||
breakpointChanged,
|
||||
layoutUpdatedEvent,
|
||||
isLayoutLocked,
|
||||
gridLayout,
|
||||
gridLayoutMultiPane,
|
||||
gridLayoutOpenTrades,
|
||||
gridLayoutTradeHistory,
|
||||
gridLayoutTradeDetail,
|
||||
gridLayoutChartView,
|
||||
responsiveGridLayouts,
|
||||
isResizableLayout,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
45
yarn.lock
45
yarn.lock
|
@ -1462,6 +1462,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@vue/composition-api/-/composition-api-1.4.9.tgz#6fa65284f545887b52d421f23b4fa1c41bc0ad4b"
|
||||
integrity sha512-l6YOeg5LEXmfPqyxAnBaCv1FMRw0OGKJ4m6nOWRm6ngt5TuHcj5ZoBRN+LXh3J0u6Ur3C4VA+RiKT+M0eItr/g==
|
||||
|
||||
"@vue/devtools-api@^6.1.4":
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.1.4.tgz#b4aec2f4b4599e11ba774a50c67fa378c9824e53"
|
||||
integrity sha512-IiA0SvDrJEgXvVxjNkHPFfDx6SXw0b/TUkqMcDZWNg9fnCAHbTpoo59YfJ9QLFkwa3raau5vSlRVzMSLDnfdtQ==
|
||||
|
||||
"@vue/eslint-config-prettier@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz#ad5912b308f4ae468458e02a2b05db0b9d246700"
|
||||
|
@ -4353,6 +4358,19 @@ pify@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
|
||||
|
||||
pinia-plugin-persistedstate@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-1.5.1.tgz#9ea81e5a245e87941c750fdb5eb303bab399427b"
|
||||
integrity sha512-X0jKWvA3kbpYe8RuIyLaZDEAFvsv3+QmBkMzInBHl0O57+eVJjswXHnIWeFAeFjktrE0cJbGHw2sBMgkcleySQ==
|
||||
|
||||
pinia@^2.0.13:
|
||||
version "2.0.13"
|
||||
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.13.tgz#6656fc290dae120a9f0cb2f5c520df400d41b8c5"
|
||||
integrity sha512-B7rSqm1xNpwcPMnqns8/gVBfbbi7lWTByzS6aPZ4JOXSJD4Y531rZHDCoYWBwLyHY/8hWnXljgiXp6rRyrofcw==
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.1.4"
|
||||
vue-demi "*"
|
||||
|
||||
pirates@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
|
||||
|
@ -5225,7 +5243,7 @@ vue-class-component@^7.2.5:
|
|||
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4"
|
||||
integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==
|
||||
|
||||
vue-demi@0.12.5, vue-demi@^0.12.1:
|
||||
vue-demi@*, vue-demi@0.12.5, vue-demi@^0.12.1:
|
||||
version "0.12.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.5.tgz#8eeed566a7d86eb090209a11723f887d28aeb2d1"
|
||||
integrity sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==
|
||||
|
@ -5273,11 +5291,6 @@ vue-material-design-icons@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/vue-material-design-icons/-/vue-material-design-icons-5.0.0.tgz#146275a05adfbd7508baf7a19b68bad77aea557c"
|
||||
integrity sha512-lYSJFW/TyQqmg7MvUbEB8ua1mwWy/v8qve7QJuA/UWUAXC4/yVUdAm4pg/sM9+k5n7VLckBv6ucOROuGBsGPDQ==
|
||||
|
||||
vue-property-decorator@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz#266a2eac61ba6527e2e68a6933cfb98fddab5457"
|
||||
integrity sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==
|
||||
|
||||
vue-router@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.3.tgz#041048053e336829d05dafacf6a8fb669a2e7999"
|
||||
|
@ -5301,26 +5314,16 @@ vue-template-es2015-compiler@^1.9.0, vue-template-es2015-compiler@^1.9.1:
|
|||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue2-helpers@^1.1.7:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/vue2-helpers/-/vue2-helpers-1.1.7.tgz#f105313979af0260ef446c583fd2fa75b067afd1"
|
||||
integrity sha512-NLF7bYFPyoKMvn/Bkxr7+7Ure/kZpWmd6pQpG613dT0Sn6EwI+2+LwVUQyDkDk4P0UaAwvn/QEYzhBRzDzuGLw==
|
||||
|
||||
vue@^2.6.14:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
|
||||
|
||||
vuex-class@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/vuex-class/-/vuex-class-0.3.2.tgz#c7e96a076c1682137d4d23a8dcfdc63f220e17a8"
|
||||
integrity sha512-m0w7/FMsNcwJgunJeM+wcNaHzK2KX1K1rw2WUQf7Q16ndXHo7pflRyOV/E8795JO/7fstyjH3EgqBI4h4n4qXQ==
|
||||
|
||||
vuex-composition-helpers@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vuex-composition-helpers/-/vuex-composition-helpers-1.1.0.tgz#a18d00192fbb0205630202aade1ec6d5f05d4c28"
|
||||
integrity sha512-36f3MWRCW6QqtP3NLyLbtTPv8qWwbac7gAK9fM4ZtDWTCWuAeBoZEiM+bmPQweAQoMM7GRSXmw/90Egiqg0DCA==
|
||||
|
||||
vuex@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
||||
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
|
||||
|
||||
w3c-hr-time@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||
|
|
Loading…
Reference in New Issue
Block a user