Merge pull request #1030 from freqtrade/vue3

Vue3 migration
This commit is contained in:
Matthias 2022-12-04 19:22:50 +01:00 committed by GitHub
commit 54f2187c23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2248 additions and 1340 deletions

View File

@ -0,0 +1,46 @@
import { setLoginInfo, defaultMocks } from './helpers';
function tradeMocks() {
cy.intercept('GET', '**/api/v1/status', { fixture: 'status_empty.json' }).as('Status');
cy.intercept('GET', '**/api/v1/profit', { fixture: 'profit.json' }).as('Profit');
cy.intercept('GET', '**/api/v1/trades*', { fixture: 'trades.json' }).as('Trades');
cy.intercept('GET', '**/api/v1/balance', { fixture: 'balance.json' }).as('Balance');
cy.intercept('GET', '**/api/v1/whitelist', { fixture: 'whitelist.json' }).as('Whitelist');
cy.intercept('GET', '**/api/v1/blacklist', { fixture: 'blacklist.json' }).as('Blacklist');
cy.intercept('GET', '**/api/v1/locks', { fixture: 'locks_empty.json' }).as('Locks');
cy.intercept('GET', '**/api/v1/performance', { fixture: 'performance.json' }).as('Performance');
// TODO: Daily mock is missing.
// cy.intercept('GET', '**/api/v1/daily', { fixture: 'performance.json' }).as('Performance');
}
describe('Dashboard', () => {
it('Dashboard view', () => {
defaultMocks();
tradeMocks();
setLoginInfo();
cy.visit('/dashboard');
cy.wait('@Ping');
cy.wait('@Status');
cy.wait('@Profit');
cy.wait('@Balance');
cy.wait('@Trades');
cy.wait('@Whitelist');
cy.wait('@Blacklist');
cy.wait('@Locks');
cy.wait('@Performance');
cy.get('.drag-header').contains('Bot comparison').should('be.visible');
cy.get('.drag-header').contains('Daily Profit').should('be.visible');
cy.get('.drag-header').contains('Open Trades').should('be.visible');
cy.get('.drag-header').contains('Cumulative Profit').should('be.visible');
// Assert Botcomparison content
cy.get('span').contains('TestBot').should('be.visible');
cy.get('span').contains('Summary').should('be.visible');
// Scroll lower
cy.get('.drag-header').contains('Closed Trades').scrollIntoView();
cy.get('.drag-header').contains('Closed Trades').should('be.visible');
cy.get('.drag-header').contains('Profit Distribution').should('be.visible');
cy.get('.drag-header').contains('Trades Log').should('be.visible');
});
});

View File

@ -31,12 +31,13 @@ describe('Login', () => {
cy.visit('/trade');
cy.location().should((loc) => {
expect(loc.pathname).to.eq('/login');
expect(loc.search).to.eq('?redirect=%2Ftrade');
expect(loc.search).to.eq('?redirect=/trade');
});
});
it('Test Login', () => {
cy.visit('/login');
cy.get('.card-header').contains('Freqtrade bot Login');
cy.get('input[id=name-input]').type('TestBot');
cy.get('input[id=username-input]').type('Freqtrader');
cy.get('input[id=password-input]').type('SuperDuperBot');
@ -93,6 +94,7 @@ describe('Login', () => {
it('Test Login failed - wrong api url', () => {
cy.visit('/login');
cy.get('.card-header').contains('Freqtrade bot Login');
cy.get('input[id=name-input]').type('TestBot');
cy.get('input[id=username-input]').type('Freqtrader');
cy.get('input[id=password-input]').type('SuperDuperBot');
@ -123,6 +125,7 @@ describe('Login', () => {
it('Test Login failed - wrong password url', () => {
cy.visit('/login');
cy.get('.card-header').contains('Freqtrade bot Login');
cy.get('input[id=name-input]').type('TestBot');
cy.get('input[id=username-input]').type('Freqtrader');
cy.get('input[id=password-input]').type('SuperDuperBot');

View File

@ -12,10 +12,11 @@ function tradeMocks() {
}
describe('Trade', () => {
it('Trade view', () => {
it('Trade view', { scrollBehavior: false }, () => {
defaultMocks();
tradeMocks();
setLoginInfo();
cy.viewport('macbook-11');
cy.visit('/trade');
cy.wait('@Ping');
@ -27,11 +28,37 @@ describe('Trade', () => {
cy.wait('@Blacklist');
cy.wait('@Locks');
cy.wait('@Performance');
cy.get('.drag-header').contains('Multi Pane').should('be.visible');
cy.get('.drag-header').contains('Chart').should('be.visible');
cy.get('button').should('contain', 'BTC/USDT');
cy.get('button').should('contain', 'ETH/USDT').should('be.visible');
cy.get('button').contains('ETH/USDT').should('be.visible');
cy.get('a[role="tab"]').contains('General').click();
// Test messageBox behavior
// No modal visible
cy.get('.modal-dialog > .modal-content > .modal-footer > .btn-secondary')
.filter(':visible')
.should('have.length', 0);
cy.get('button[title*="Stop Trading"]').click();
// Modal open
cy.get('.modal-dialog > .modal-content > .modal-footer > .btn-secondary')
.filter(':visible')
.contains('Cancel')
.should('be.visible')
.click();
// Modal closed
cy.get('.modal-dialog > .modal-content > .modal-footer > .btn-secondary')
.filter(':visible')
.should('have.length', 0);
// General
cy.get('button[role="tab"]').contains('General').click();
cy.get('button').contains('ETH/USDT').should('not.be.visible');
// 2nd segment
cy.get('.drag-header').contains('Open Trades').scrollIntoView().should('be.visible');
cy.get('.drag-header').contains('Closed Trades').scrollIntoView().should('be.visible');
cy.get('span').contains('TRX/USDT').should('be.visible');
cy.get('td').contains('8070.5').should('be.visible');
});
});

View File

@ -19,23 +19,23 @@ import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
import { mount } from 'cypress/vue2';
// Vue2.7 Workaround to have proper types
import { mount as mount3 } from 'cypress/vue';
import { mount } from 'cypress/vue';
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount3;
}
}
}
// import '../../src/assets/main.css';
Cypress.Commands.add('mount', mount);
// Example use:
// cy.mount(MyComponent)
// cy.mount(MyComponent);s

View File

@ -16,9 +16,10 @@
"cy:run-ct": "cypress run --component"
},
"dependencies": {
"@popperjs/core": "^2.11.6",
"axios": "^1.2.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"bootstrap": "^5.2.2",
"bootstrap-vue-3": "^0.4.11",
"bootswatch": "^5.2.2",
"core-js": "^3.26.1",
"date-fns": "^2.29.3",
@ -28,35 +29,34 @@
"humanize-duration": "^3.27.3",
"pinia": "^2.0.27",
"pinia-plugin-persistedstate": "^3.0.1",
"vue": "^2.7.14",
"vue": "3.2.42",
"vue-class-component": "^7.2.5",
"vue-demi": "0.13.11",
"vue-echarts": "^6.2.3",
"vue-grid-layout": "^2.4.0",
"vue-material-design-icons": "^5.1.2",
"vue-router": "^3.6.5",
"vue-select": "^3.20.0"
"vue-router": "^4.1.6",
"vue-select": "^4.0.0-beta.5",
"vue3-drr-grid-layout": "^1.9.7"
},
"devDependencies": {
"@cypress/vite-dev-server": "^4.0.1",
"@cypress/vue": "^2.2.3",
"@cypress/vue": "^5.0.2",
"@types/echarts": "^4.9.16",
"@types/jest": "^27.5.0",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitejs/plugin-vue2": "^1.1.2",
"@vue/composition-api": "^1.7.1",
"@vitejs/plugin-vue": "^1.2.2",
"@vue/compiler-sfc": "3.2.42",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.1.0",
"@vue/runtime-dom": "^3.2.45",
"@vue/test-utils": "^1.3.3",
"cypress": "^10.3.0",
"@vue/test-utils": "^2.2.4",
"cypress": "^11.2.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.20.0",
"jest": "^27.5.1",
"mutationobserver-shim": "^0.3.7",
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
"prettier": "^2.8.0",
"sass": "^1.56.1",
@ -65,7 +65,6 @@
"typescript": "~4.9.3",
"vite": "^2.9.14",
"vite-jest": "^0.1.4",
"vue-template-compiler": "^2.7.14",
"vue-tsc": "^1.0.10"
}
}

View File

@ -1,30 +1,39 @@
<template>
<div class="d-flex align-items-center justify-content-between w-100">
<span class="mr-2">{{ bot.botName || bot.botId }}</span>
<div v-if="bot" class="d-flex align-items-center justify-content-between w-100">
<span class="me-2">{{ bot.botName || bot.botId }}</span>
<div class="align-items-center d-flex">
<span class="ml-2 mr-1 align-middle">{{
botStore.botStores[bot.botId].isBotOnline ? '&#128994;' : '&#128308;'
}}</span>
<b-form-checkbox
v-model="autoRefreshLoc"
class="ml-auto float-right mr-2 my-auto"
class="ms-auto float-end me-2 my-auto mt-1"
title="AutoRefresh"
variant="secondary"
switch
@change="changeEvent"
>
R
<span class="ms-2 me-1 align-middle">{{
botStore.botStores[bot.botId].isBotOnline ? '&#128994;' : '&#128308;'
}}</span>
</b-form-checkbox>
<div v-if="!noButtons" class="d-flex flex-align-cent">
<b-button class="ml-1" size="sm" title="Delete bot" @click="$emit('edit')">
<div v-if="!noButtons" class="float-end d-flex flex-align-center">
<b-button class="ms-1" size="sm" title="Delete bot" @click="$emit('edit')">
<EditIcon :size="16" />
</b-button>
<b-button class="ml-1" size="sm" title="Delete bot" @click.prevent="clickRemoveBot(bot)">
<b-button class="ms-1" size="sm" title="Delete bot" @click="botRemoveModalVisible = true">
<DeleteIcon :size="16" title="Delete Bot" />
</b-button>
</div>
</div>
<b-modal
v-if="!noButtons"
id="removeBotModal"
v-model="botRemoveModalVisible"
title="Logout confirmation"
@ok="confirmRemoveBot"
>
Really remove (logout) from {{ bot.botName }} ({{ bot.botId }})?
</b-modal>
</div>
</template>
@ -32,7 +41,7 @@
import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import { BotDescriptor } from '@/types';
import { defineComponent, computed, getCurrentInstance } from 'vue';
import { defineComponent, computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
@ -47,22 +56,17 @@ export default defineComponent({
},
emits: ['edit'],
setup(props) {
const root = getCurrentInstance();
const botStore = useBotStore();
const changeEvent = (v) => {
botStore.botStores[props.bot.botId].setAutoRefresh(v);
};
const botRemoveModalVisible = ref(false);
const clickRemoveBot = (bot: BotDescriptor) => {
//
root?.proxy.$bvModal
.msgBoxConfirm(`Really remove (logout) from '${bot.botName}' (${bot.botId})?`)
.then((value: boolean) => {
if (value) {
botStore.removeBot(bot.botId);
}
});
const confirmRemoveBot = () => {
botRemoveModalVisible.value = false;
botStore.removeBot(props.bot.botId);
console.log('removing bot.');
};
const autoRefreshLoc = computed({
get() {
@ -76,9 +80,18 @@ export default defineComponent({
return {
botStore,
changeEvent,
clickRemoveBot,
autoRefreshLoc,
confirmRemoveBot,
botRemoveModalVisible,
};
},
});
</script>
<style scoped lang="scss">
.form-switch {
padding-left: 0;
display: flex;
flex-wrap: nowrap;
}
</style>

View File

@ -9,12 +9,12 @@
autofocus
/>
<div class="d-flex ml-2">
<div class="d-flex ms-2">
<b-button type="submit" size="sm" title="Save">
<CheckIcon :size="16" />
</b-button>
<b-button class="ml-1" size="sm" title="Cancel" @click="$emit('cancelled')">
<b-button class="ms-1" size="sm" title="Cancel" @click="$emit('cancelled')">
<CloseIcon :size="16" />
</b-button>
</div>

View File

@ -18,8 +18,9 @@
<b-form-input
id="url-input"
v-model="auth.url"
:state="urlState"
required
trim
:state="urlState === '' ? null : urlState"
@keydown.enter.native="handleOk"
></b-form-input>
</b-form-group>
@ -34,6 +35,7 @@
v-model="auth.username"
required
placeholder="Freqtrader"
:state="nameState === '' ? null : nameState"
@keydown.enter.native="handleOk"
></b-form-input>
</b-form-group>
@ -48,6 +50,7 @@
v-model="auth.password"
required
type="password"
:state="pwdState === '' ? null : pwdState"
@keydown.enter.native="handleOk"
></b-form-input>
</b-form-group>
@ -63,8 +66,8 @@
>
</b-alert>
</div>
<div v-if="inModal === false" class="float-right">
<b-button class="mr-2" type="reset" variant="danger">Reset</b-button>
<div v-if="inModal === false" class="float-end">
<b-button class="me-2" type="reset" variant="danger">Reset</b-button>
<b-button type="submit" variant="primary">Submit</b-button>
</div>
</form>
@ -77,8 +80,8 @@ import { useUserService } from '@/shared/userService';
import { AuthPayload } from '@/types';
import { defineComponent, ref } from 'vue';
import { useRouter, useRoute } from '@/composables/router-helper';
import { useBotStore } from '@/stores/ftbotwrapper';
import { useRoute, useRouter } from 'vue-router';
const defaultURL = window.location.origin || 'http://localhost:3000';
@ -93,9 +96,9 @@ export default defineComponent({
const route = useRoute();
const botStore = useBotStore();
const nameState = ref<boolean | null>();
const pwdState = ref<boolean | null>();
const urlState = ref<boolean | null>();
const nameState = ref<boolean | ''>('');
const pwdState = ref<boolean | ''>('');
const urlState = ref<boolean | ''>('');
const errorMessage = ref<string>('');
const errorMessageCORS = ref<boolean>(false);
const formRef = ref<HTMLFormElement>();
@ -121,8 +124,9 @@ export default defineComponent({
auth.value.url = defaultURL;
auth.value.username = '';
auth.value.password = '';
nameState.value = null;
pwdState.value = null;
nameState.value = '';
pwdState.value = '';
urlState.value = '';
errorMessage.value = '';
};
@ -158,10 +162,10 @@ export default defineComponent({
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 {
if (resolved.name === '404') {
router.push('/');
} else {
router.push(resolved.path);
}
} else {
router.push('/');

View File

@ -1,5 +1,5 @@
<template>
<div class="row flex-grow-1 chart-wrapper">
<div class="d-flex flex-grow-1 chart-wrapper">
<v-chart v-if="hasData" ref="candleChart" :theme="theme" autoresize manual-update />
</div>
</template>

View File

@ -1,23 +1,12 @@
<template>
<div class="d-flex h-100">
<div class="flex-fill container-fluid flex-column align-items-stretch d-flex h-100">
<b-modal
v-if="plotConfigModal"
id="plotConfiguratorModal"
title="Plot Configurator"
ok-only
hide-backdrop
button-size="sm"
>
<PlotConfigurator :columns="datasetColumns" />
</b-modal>
<div class="row mr-0">
<div class="ml-2 d-flex flex-wrap flex-md-nowrap align-items-center">
<span class="ml-2 text-nowrap">{{ strategyName }} | {{ timeframe || '' }}</span>
<div class="flex-fill w-100 flex-column align-items-stretch d-flex h-100">
<div class="d-flex me-0">
<div class="ms-2 d-flex flex-wrap flex-md-nowrap align-items-center w-auto">
<span class="ms-2 text-nowrap">{{ strategyName }} | {{ timeframe || '' }}</span>
<v-select
v-model="pair"
class="ml-2"
class="ms-2"
:options="availablePairs"
style="min-width: 7em"
size="sm"
@ -26,26 +15,26 @@
>
</v-select>
<b-button class="ml-2" :disabled="!!!pair" size="sm" @click="refresh">&#x21bb;</b-button>
<small v-if="dataset" class="ml-2 text-nowrap" title="Long entry signals"
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">&#x21bb;</b-button>
<small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals"
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
>
<small v-if="dataset" class="ml-2 text-nowrap" title="Long exit signals"
<small v-if="dataset" class="ms-2 text-nowrap" title="Long exit signals"
>Long exit: {{ dataset.exit_long_signals || dataset.sell_signals }}</small
>
<small v-if="dataset && dataset.enter_short_signals" class="ml-2 text-nowrap"
<small v-if="dataset && dataset.enter_short_signals" class="ms-2 text-nowrap"
>Short entries: {{ dataset.enter_short_signals }}</small
>
<small v-if="dataset && dataset.exit_short_signals" class="ml-2 text-nowrap"
<small v-if="dataset && dataset.exit_short_signals" class="ms-2 text-nowrap"
>Short exits: {{ dataset.exit_short_signals }}</small
>
</div>
<div class="ml-auto d-flex align-items-center">
<div class="ms-auto d-flex align-items-center w-auto">
<b-form-checkbox v-model="settingsStore.useHeikinAshiCandles"
>Heikin Ashi</b-form-checkbox
>
<div class="ml-2">
<div class="ms-2">
<b-form-select
v-model="plotStore.plotConfigName"
:options="plotStore.availablePlotConfigNames"
@ -55,14 +44,14 @@
</b-form-select>
</div>
<div class="ml-2 mr-0 mr-md-1">
<div class="ms-2 me-0 me-md-1">
<b-button size="sm" title="Plot configurator" @click="showConfigurator">
&#9881;
</b-button>
</div>
</div>
</div>
<div class="row mr-1 ml-1 h-100">
<div class="me-1 ms-1 h-100">
<CandleChart
v-if="hasDataset"
:dataset="dataset"
@ -88,6 +77,16 @@
<PlotConfigurator :columns="datasetColumns" :as-modal="false" />
</div>
</transition>
<b-modal
v-if="plotConfigModal"
id="plotConfiguratorModal"
v-model="showPlotConfigModal"
title="Plot Configurator"
ok-only
hide-backdrop
>
<PlotConfigurator :columns="datasetColumns" />
</b-modal>
</div>
</template>
@ -99,7 +98,7 @@ import vSelect from 'vue-select';
import { useSettingsStore } from '@/stores/settings';
import { usePlotConfigStore } from '@/stores/plotConfig';
import { defineComponent, ref, computed, onMounted, watch, getCurrentInstance } from 'vue';
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
@ -122,7 +121,6 @@ export default defineComponent({
},
},
setup(props) {
const root = getCurrentInstance();
const settingsStore = useSettingsStore();
const botStore = useBotStore();
const plotStore = usePlotConfigStore();
@ -165,10 +163,10 @@ export default defineComponent({
return 'Unknown';
}
});
const showPlotConfigModal = ref(false);
const showConfigurator = () => {
if (props.plotConfigModal) {
root?.proxy.$bvModal.show('plotConfiguratorModal');
showPlotConfigModal.value = !showPlotConfigModal.value;
} else {
showPlotConfig.value = !showPlotConfig.value;
}
@ -239,6 +237,7 @@ export default defineComponent({
showConfigurator,
refresh,
pair,
showPlotConfigModal,
};
},
});

View File

@ -47,7 +47,7 @@
title="Remove indicator to plot"
size="sm"
:disabled="!selIndicatorName"
class="ml-1"
class="ms-1"
@click="removeIndicator"
>
Remove indicator
@ -64,13 +64,13 @@
<hr />
<div>
<b-button class="ml-1" variant="secondary" size="sm" @click="loadPlotConfig">Load</b-button>
<b-button class="ml-1" variant="secondary" size="sm" @click="loadPlotConfigFromStrategy">
<b-button class="ms-1" variant="secondary" size="sm" @click="loadPlotConfig">Load</b-button>
<b-button class="ms-1" variant="secondary" size="sm" @click="loadPlotConfigFromStrategy">
From strategy
</b-button>
<b-button
class="ml-1"
class="ms-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@ -79,7 +79,7 @@
>
<b-button
id="showButton"
class="ml-1"
class="ms-1"
variant="secondary"
size="sm"
title="Show configuration for easy transfer to a strategy"
@ -89,7 +89,7 @@
<b-button
v-if="showConfig"
class="ml-1"
class="ms-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@ -97,7 +97,7 @@
>Load from String</b-button
>
<b-button
class="ml-1"
class="ms-1"
variant="primary"
size="sm"
data-toggle="tooltip"
@ -106,7 +106,7 @@
>Save</b-button
>
</div>
<div v-if="showConfig" class="col-mb-5 ml-1 mt-2">
<div v-if="showConfig" class="col-mb-5 ms-1 mt-2">
<b-form-textarea
id="TextArea"
v-model="plotConfigJson"

View File

@ -6,7 +6,7 @@
<b-form-input v-model="indicatorFilter" placeholder="Filter indicators"></b-form-input>
<b-input-group-append>
<Reset
class="pointer align-self-center ml-1"
class="pointer align-self-center ms-1"
:size="18"
@click="indicatorFilter = ''"
></Reset>
@ -36,7 +36,7 @@
<b-form-group label="Color" label-for="colsel" size="sm">
<b-input-group>
<b-input-group-prepend>
<div :style="{ 'background-color': selColor }" class="colorbox mr-2"></div>
<div :style="{ 'background-color': selColor }" class="colorbox me-2"></div>
<!-- <b-form-input
id="colsel"
v-model="selColor"
@ -67,7 +67,7 @@
</b-button>
<b-button
v-if="addNew"
class="ml-1 flex-grow-1"
class="ms-1 flex-grow-1"
variant="secondary"
title="Add "
size="sm"
@ -92,11 +92,11 @@ export default defineComponent({
Reset,
},
props: {
value: { required: true, type: Object as () => Record<string, IndicatorConfig> },
modelValue: { required: true, type: Object as () => Record<string, IndicatorConfig> },
columns: { required: true, type: Array as () => string[] },
addNew: { required: true, type: Boolean },
},
emits: ['input'],
emits: ['update:modelValue'],
setup(props, { emit }) {
const selColor = ref(randomColor());
const graphType = ref<ChartType>(ChartType.line);
@ -127,7 +127,7 @@ export default defineComponent({
};
});
const emitIndicator = () => {
emit('input', combinedIndicator.value);
emit('update:modelValue', combinedIndicator.value);
};
const clickCancel = () => {
@ -136,13 +136,13 @@ export default defineComponent({
};
watch(
() => props.value,
() => props.modelValue,
() => {
[selAvailableIndicator.value] = Object.keys(props.value);
[selAvailableIndicator.value] = Object.keys(props.modelValue);
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;
if (selAvailableIndicator.value && props.modelValue) {
selColor.value = props.modelValue[selAvailableIndicator.value].color || randomColor();
graphType.value = props.modelValue[selAvailableIndicator.value].type || ChartType.line;
}
},
);

View File

@ -5,7 +5,7 @@
</div>
<b-form-group
class="w-25 order-1"
:class="showTitle ? 'ml-5 pl-5' : 'position-absolute'"
:class="showTitle ? 'ms-5 ps-5' : 'position-absolute'"
label="Bins"
label-for="input-bins"
label-cols="6"

View File

@ -1,7 +1,7 @@
<template>
<div>
<button
class="btn btn-secondary float-right"
class="btn btn-secondary float-end"
title="Refresh"
aria-label="Refresh"
@click="botStore.activeBot.getBacktestHistory"
@ -12,7 +12,7 @@
Load Historic results from disk. You can click on multiple results to load all of them into
freqUI.
</p>
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ml-2">
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ms-2">
<b-list-group-item
v-for="(res, idx) in botStore.activeBot.backtestHistoryList"
:key="idx"

View File

@ -1,13 +1,13 @@
<template>
<div>
<div class="row">
<div class="col-md-11 text-left">
<div class="col-md-11 text-start">
<p>
Graph will always show the latest values for the selected strategy. Timerange:
{{ timerange }} - {{ strategy }}
</p>
</div>
<div class="col-md-1 text-right">
<div class="col-md-1 text-end">
<b-button
aria-label="Close"
title="Trade Navigation"

View File

@ -1,7 +1,7 @@
<template>
<div class="container d-flex flex-column align-items-center">
<h3>Available results:</h3>
<b-list-group class="ml-2">
<b-list-group class="ms-2">
<b-list-group-item
v-for="[key, strat] in Object.entries(backtestHistory)"
:key="key"

View File

@ -4,9 +4,9 @@
<h3>Backtest-result for {{ backtestResult.strategy_name }}</h3>
</div>
<div class="row text-left ml-0">
<div class="row text-start ms-0">
<div class="row w-100">
<div class="col-12 col-xl-6 px-0 px-xl-0 pr-xl-1">
<div class="col-12 col-xl-6 px-0 px-xl-0 pe-xl-1">
<b-card header="Strategy settings">
<b-table
small
@ -17,7 +17,7 @@
</b-table>
</b-card>
</div>
<div class="col-12 col-xl-6 px-0 px-xl-0 pt-2 pt-xl-0 pl-xl-1">
<div class="col-12 col-xl-6 px-0 px-xl-0 pt-2 pt-xl-0 ps-xl-1">
<b-card header="Metrics">
<b-table small borderless :items="backtestResultStats" :fields="backtestResultFields">
</b-table>

View File

@ -1,13 +1,13 @@
<template>
<div>
<div class="mb-2">
<label class="mr-auto h3">Balance</label>
<b-button class="float-right" size="sm" @click="botStore.activeBot.getBalance"
<label class="me-auto h3">Balance</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance"
>&#x21bb;</b-button
>
<b-form-checkbox
v-model="hideSmallBalances"
class="float-right"
class="float-end"
size="sm"
title="Hide small balances"
button

View File

@ -3,28 +3,16 @@
<b-alert
v-for="(alert, index) in alertStore.activeMessages"
:key="index"
v-model="alert.timeout"
variant="warning"
dismissible
:show="5"
:value="!!alert.message"
@dismissed="alertStore.removeAlert"
@closed="alertStore.removeAlert(alert)"
>{{ alert.message }}</b-alert
>
{{ alert.message }}
</b-alert>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { useAlertsStore } from '@/stores/alerts';
export default defineComponent({
name: 'BotAlerts',
setup() {
const alertStore = useAlertsStore();
return {
alertStore,
};
},
});
const alertStore = useAlertsStore();
</script>

View File

@ -2,7 +2,7 @@ forceexit
<template>
<div>
<button
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading || isRunning"
title="Start Trading"
@click="botStore.activeBot.startBot()"
@ -10,7 +10,7 @@ forceexit
<PlayIcon />
</button>
<button
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading || !isRunning"
title="Stop Trading - Also stops handling open trades."
@click="handleStopBot()"
@ -18,7 +18,7 @@ forceexit
<StopIcon />
</button>
<button
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading || !isRunning"
title="StopBuy - Stops buying, but still handles open trades"
@click="handleStopBuy()"
@ -26,7 +26,7 @@ forceexit
<PauseIcon />
</button>
<button
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading"
title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly."
@click="handleReloadConfig()"
@ -34,7 +34,7 @@ forceexit
<ReloadIcon />
</button>
<button
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading"
title="Force exit all"
@click="handleForceExit()"
@ -47,23 +47,24 @@ forceexit
(botStore.activeBot.botState.force_entry_enable ||
botStore.activeBot.botState.forcebuy_enabled)
"
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading || !isRunning"
title="Force enter - Immediately enter a trade at an optional price. Exits are then handled according to strategy rules."
@click="initiateForceenter"
@click="forceEnter = true"
>
<ForceEntryIcon />
</button>
<button
v-if="botStore.activeBot.isWebserverMode && false"
:disabled="botStore.activeBot.isTrading"
class="btn btn-secondary btn-sm ml-1"
class="btn btn-secondary btn-sm ms-1"
title="Start Trading mode"
@click="botStore.activeBot.startTrade()"
>
<PlayIcon />
</button>
<ForceEntryForm :pair="botStore.activeBot.selectedPair" @close="hideForceenter" />
<ForceEntryForm v-model="forceEnter" :pair="botStore.activeBot.selectedPair" />
<MessageBox ref="msgBox" />
</div>
</template>
@ -76,7 +77,8 @@ import ReloadIcon from 'vue-material-design-icons/Reload.vue';
import ForceExitIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
import ForceEntryIcon from 'vue-material-design-icons/PlusBoxMultipleOutline.vue';
import ForceEntryForm from './ForceEntryForm.vue';
import { defineComponent, computed, ref, getCurrentInstance } from 'vue';
import MessageBox, { MsgBoxObject } from '@/components/general/MessageBox.vue';
import { defineComponent, computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
@ -89,70 +91,74 @@ export default defineComponent({
ReloadIcon,
ForceExitIcon,
ForceEntryIcon,
MessageBox,
},
setup() {
const root = getCurrentInstance();
const botStore = useBotStore();
const forcebuyShow = ref(false);
const forceEnter = ref<boolean>(false);
const msgBox = ref<typeof MessageBox>();
const isRunning = computed((): boolean => {
return botStore.activeBot.botState?.state === 'running';
});
const initiateForceenter = () => {
root?.proxy.$bvModal.show('forceentry-modal');
};
const hideForceenter = () => {
root?.proxy.$bvModal.hide('forceentry-modal');
};
const handleStopBot = () => {
root?.proxy.$bvModal.msgBoxConfirm('Stop Bot?').then((value: boolean) => {
if (value) {
const msg: MsgBoxObject = {
title: 'Stop Bot',
message: 'Stop the bot loop from running?',
accept: () => {
botStore.activeBot.stopBot();
}
});
},
};
msgBox.value?.show(msg);
};
const handleStopBuy = () => {
root?.proxy.$bvModal
.msgBoxConfirm('Stop buying? Freqtrade will continue to handle open trades.')
.then((value: boolean) => {
if (value) {
botStore.activeBot.stopBuy();
}
});
const msg: MsgBoxObject = {
title: 'Stop Buying',
message: 'Freqtrade will continue to handle open trades.',
accept: () => {
botStore.activeBot.stopBuy();
},
};
msgBox.value?.show(msg);
};
const handleReloadConfig = () => {
root?.proxy.$bvModal.msgBoxConfirm('Reload configuration?').then((value: boolean) => {
if (value) {
const msg: MsgBoxObject = {
title: 'Reload',
message: 'Reload configuration (including strategy)?',
accept: () => {
console.log('reload...');
botStore.activeBot.reloadConfig();
}
});
},
};
msgBox.value?.show(msg);
};
const handleForceExit = () => {
root?.proxy.$bvModal.msgBoxConfirm(`Really forceexit ALL trades?`).then((value: boolean) => {
if (value) {
const msg: MsgBoxObject = {
title: 'ForceExit all',
message: 'Really forceexit ALL trades?',
accept: () => {
const payload: ForceSellPayload = {
tradeid: 'all',
// TODO: support ordertype (?)
};
botStore.activeBot.forceexit(payload);
}
});
},
};
msgBox.value?.show(msg);
};
return {
initiateForceenter,
hideForceenter,
handleStopBot,
handleStopBuy,
handleReloadConfig,
handleForceExit,
forcebuyShow,
forceEnter,
botStore,
isRunning,
msgBox,
};
},
});

View File

@ -1,10 +1,10 @@
<template>
<div class="d-flex">
<div
class="px-1 d-flex flex-row flex-fill text-left justify-content-between align-items-center"
class="px-1 d-flex flex-row flex-fill text-start justify-content-between align-items-center"
>
<span>
<span class="mr-1 font-weight-bold">{{ trade.pair }}</span>
<span class="me-1 fw-bold">{{ trade.pair }}</span>
<small class="text-secondary">(#{{ trade.trade_id }})</small>
</span>
<small>

View File

@ -1,10 +1,8 @@
<template>
<div>
<div class="mb-2">
<label class="mr-auto h3">Daily Stats</label>
<b-button class="float-right" size="sm" @click="botStore.activeBot.getDaily"
>&#x21bb;</b-button
>
<label class="me-auto h3">Daily Stats</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getDaily">&#x21bb;</b-button>
</div>
<div>
<DailyChart

View File

@ -23,14 +23,14 @@
<!-- Blacklsit -->
<div>
<label
class="mr-auto h3"
class="me-auto h3"
title="Blacklist - Select (followed by a click on '-') to remove pairs"
>Blacklist</label
>
<div class="float-right d-flex d-flex-columns pr-1">
<div class="float-end d-flex d-flex-columns pe-1">
<b-button
id="blacklist-add-btn"
class="mr-1"
class="me-1"
:class="botStore.activeBot.botApiVersion >= 1.12 ? 'col-6' : ''"
size="sm"
>+
@ -49,7 +49,7 @@
<b-popover
title="Add to blacklist"
target="blacklist-add-btn"
triggers="click blur"
triggers="click"
:show.sync="blackListShow"
>
<form ref="form" @submit.prevent>
@ -64,7 +64,7 @@
</b-form-group>
<b-button
id="blacklist-submit"
class="float-right mb-2"
class="float-end mb-2"
size="sm"
type="submit"
@click="addBlacklistPair"

View File

@ -1,130 +1,146 @@
<template>
<div>
<b-modal
id="forceentry-modal"
ref="modal"
title="Force entering a trade"
@show="resetForm"
@hidden="resetForm"
@ok="handleEntry"
>
<form ref="form" @submit.stop.prevent="handleSubmit">
<b-form-group
v-if="botStore.activeBot.botApiVersion >= 2.13 && botStore.activeBot.shortAllowed"
label="Order direction (Long or Short)"
label-for="order-direction"
invalid-feedback="Order direction must be empty or a positive number"
>
<b-form-radio-group
id="order-direction"
v-model="orderSide"
:options="['long', 'short']"
name="radios-btn-default"
size="sm"
buttons
style="min-width: 10em"
button-variant="outline-primary"
></b-form-radio-group>
</b-form-group>
<b-form-group label="Pair" label-for="pair-input" invalid-feedback="Pair is required">
<b-form-input
id="pair-input"
v-model="selectedPair"
required
@keydown.enter.native="handleEntry"
@focus="inputSelect"
></b-form-input>
</b-form-group>
<b-form-group
label="*Price [optional]"
label-for="price-input"
invalid-feedback="Price must be empty or a positive number"
>
<b-form-input
id="price-input"
v-model="price"
type="number"
step="0.00000001"
@keydown.enter.native="handleEntry"
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*Stake-amount in ${botStore.activeBot.stakeCurrency} [optional]`"
label-for="stake-input"
invalid-feedback="Stake-amount must be empty or a positive number"
>
<b-form-input
id="stake-input"
v-model="stakeAmount"
type="number"
step="0.000001"
@keydown.enter.native="handleEntry"
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 2.16 && botStore.activeBot.shortAllowed"
:label="`*Leverage to apply [optional]`"
label-for="leverage-input"
invalid-feedback="Leverage must be empty or a positive number"
>
<b-form-input
id="leverage-input"
v-model="leverage"
type="number"
step="0.01"
@keydown.enter.native="handleEntry"
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.1"
label="*OrderType"
label-for="ordertype-input"
invalid-feedback="OrderType"
>
<b-form-radio-group
id="ordertype-input"
v-model="ordertype"
:options="['market', 'limit']"
name="radios-btn-default"
buttons
button-variant="outline-primary"
style="min-width: 10em"
size="sm"
></b-form-radio-group>
</b-form-group>
</form>
</b-modal>
</div>
<b-modal
id="forceentry-modal"
ref="modal"
v-model="model"
title="Force entering a trade"
@show="resetForm"
@hidden="resetForm"
@ok="handleEntry"
>
<form ref="form" @submit.stop.prevent="handleSubmit">
<b-form-group
v-if="botStore.activeBot.botApiVersion >= 2.13 && botStore.activeBot.shortAllowed"
label="Order direction (Long or Short)"
label-for="order-direction"
invalid-feedback="Order direction must be set"
:state="orderSide !== undefined"
>
<b-form-radio-group
id="order-direction"
v-model="orderSide"
:options="['long', 'short']"
name="radios-btn-default"
size="sm"
buttons
style="min-width: 10em"
button-variant="outline-primary"
></b-form-radio-group>
</b-form-group>
<b-form-group
label="Pair"
label-for="pair-input"
invalid-feedback="Pair is required"
:state="selectedPair !== undefined"
>
<b-form-input
id="pair-input"
v-model="selectedPair"
required
@keydown.enter.native="handleEntry"
@focus="inputSelect"
></b-form-input>
</b-form-group>
<b-form-group
label="*Price [optional]"
label-for="price-input"
invalid-feedback="Price must be empty or a positive number"
:state="!price || price > 0"
>
<b-form-input
id="price-input"
v-model="price"
type="number"
step="0.00000001"
@keydown.enter.native="handleEntry"
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*Stake-amount in ${botStore.activeBot.stakeCurrency} [optional]`"
label-for="stake-input"
invalid-feedback="Stake-amount must be empty or a positive number"
:state="!stakeAmount || stakeAmount > 0"
>
<b-form-input
id="stake-input"
v-model="stakeAmount"
type="number"
step="0.000001"
@keydown.enter.native="handleEntry"
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 2.16 && botStore.activeBot.shortAllowed"
:label="`*Leverage to apply [optional]`"
label-for="leverage-input"
invalid-feedback="Leverage must be empty or a positive number"
:state="!leverage || leverage > 0"
>
<b-form-input
id="leverage-input"
v-model="leverage"
type="number"
step="0.01"
@keydown.enter.native="handleEntry"
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.1"
label="*OrderType"
label-for="ordertype-input"
invalid-feedback="OrderType"
:state="true"
>
<b-form-radio-group
id="ordertype-input"
v-model="ordertype"
:options="['market', 'limit']"
name="radios-btn-default"
buttons
button-variant="outline-primary"
style="min-width: 10em"
size="sm"
></b-form-radio-group>
</b-form-group>
</form>
</b-modal>
</template>
<script lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { ForceEnterPayload, OrderSides } from '@/types';
import { defineComponent, ref, nextTick, getCurrentInstance } from 'vue';
import { computed, defineComponent, nextTick, ref } from 'vue';
export default defineComponent({
name: 'ForceEntryForm',
props: {
pair: {
type: String,
default: '',
},
modelValue: { required: true, default: false, type: Boolean },
pair: { type: String, default: '' },
},
setup(props) {
const root = getCurrentInstance();
emits: ['update:modelValue'],
setup(props, { emit }) {
const botStore = useBotStore();
const form = ref<HTMLFormElement>();
const selectedPair = ref('');
const price = ref<number | null>(null);
const stakeAmount = ref<number | null>(null);
const leverage = ref<number | null>(null);
const price = ref<number | undefined>(undefined);
const stakeAmount = ref<number | undefined>(undefined);
const leverage = ref<number | undefined>(undefined);
const ordertype = ref('');
const orderSide = ref<OrderSides>(OrderSides.long);
const model = computed({
get() {
return props.modelValue;
},
set(value: boolean) {
emit('update:modelValue', value);
},
});
const checkFormValidity = () => {
const valid = form.value?.checkValidity();
@ -155,15 +171,14 @@ export default defineComponent({
payload.leverage = leverage.value;
}
botStore.activeBot.forceentry(payload);
await nextTick();
root?.proxy.$bvModal.hide('forceentry-modal');
emit('update:modelValue', false);
};
const resetForm = () => {
console.log('resetForm');
selectedPair.value = props.pair;
price.value = null;
stakeAmount.value = null;
price.value = undefined;
stakeAmount.value = undefined;
if (botStore.activeBot.botApiVersion > 1.1) {
ordertype.value =
botStore.activeBot.botState?.order_types?.forcebuy ||
@ -174,9 +189,7 @@ export default defineComponent({
}
};
const handleEntry = (bvModalEvt) => {
// Prevent modal from closing
bvModalEvt.preventDefault();
const handleEntry = () => {
// Trigger submit handler
handleSubmit();
};
@ -186,6 +199,7 @@ export default defineComponent({
return {
handleSubmit,
model,
botStore,
form,
handleEntry,

View File

@ -2,7 +2,7 @@
<div>
<b-modal
id="forceexit-modal"
ref="modal"
v-model="model"
title="Force exiting a trade"
@show="resetForm"
@hidden="resetForm"
@ -16,9 +16,10 @@
</p>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*amount in ${trade.base_currency} [optional]`"
:label="`*Amount in ${trade.base_currency} [optional]`"
label-for="stake-input"
invalid-feedback="Amount must be empty or a positive number"
:state="amount !== undefined && amount > 0"
>
<b-form-input
id="stake-input"
@ -43,10 +44,11 @@
label="*OrderType"
label-for="ordertype-input"
invalid-feedback="OrderType"
:state="ordertype !== undefined"
>
<b-form-select
v-model="ordertype"
class="ml-2"
class="ms-2"
:options="['market', 'limit']"
style="min-width: 7em"
size="sm"
@ -62,7 +64,7 @@
import { useBotStore } from '@/stores/ftbotwrapper';
import { ForceSellPayload, Trade } from '@/types';
import { defineComponent, ref, nextTick, getCurrentInstance } from 'vue';
import { defineComponent, ref, computed } from 'vue';
export default defineComponent({
name: 'ForceExitForm',
@ -71,13 +73,14 @@ export default defineComponent({
type: Object as () => Trade,
required: true,
},
modelValue: { required: true, default: false, type: Boolean },
},
setup(props) {
const root = getCurrentInstance();
emits: ['update:modelValue'],
setup(props, { emit }) {
const botStore = useBotStore();
const form = ref<HTMLFormElement>();
const amount = ref<number | null>(null);
const amount = ref<number | undefined>(undefined);
const ordertype = ref('limit');
const checkFormValidity = () => {
@ -86,6 +89,15 @@ export default defineComponent({
return valid;
};
const model = computed({
get() {
return props.modelValue;
},
set(value: boolean) {
emit('update:modelValue', value);
},
});
const handleSubmit = () => {
// Exit when the form isn't valid
if (!checkFormValidity()) {
@ -101,9 +113,7 @@ export default defineComponent({
payload.amount = amount.value;
}
botStore.activeBot.forceexit(payload);
nextTick(() => {
root?.proxy.$bvModal.hide('forceexit-modal');
});
model.value = false;
};
const resetForm = () => {
amount.value = props.trade.amount;
@ -115,9 +125,7 @@ export default defineComponent({
}
};
const handleEntry = (bvModalEvt) => {
// Prevent modal from closing
bvModalEvt.preventDefault();
const handleEntry = () => {
// Trigger submit handler
handleSubmit();
};
@ -129,6 +137,7 @@ export default defineComponent({
resetForm,
amount,
ordertype,
model,
};
},
});

View File

@ -1,16 +1,14 @@
<template>
<div>
<div class="mb-2">
<label class="mr-auto h3">Pair Locks</label>
<b-button class="float-right" size="sm" @click="botStore.activeBot.getLocks"
>&#x21bb;</b-button
>
<label class="me-auto h3">Pair Locks</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getLocks">&#x21bb;</b-button>
</div>
<div>
<b-table class="table-sm" :items="botStore.activeBot.activeLocks" :fields="tableFields">
<template #cell(actions)="row">
<b-button
class="btn-xs ml-1"
class="btn-xs ms-1"
size="sm"
title="Delete trade"
@click="removePairLock(row.item)"

View File

@ -1,8 +1,8 @@
<template>
<div class="d-flex flex-align-center ml-2">
<div class="d-flex align-items-center ms-2">
<b-form-checkbox
v-model="autoRefreshLoc"
class="ml-auto float-right my-auto"
class="ms-auto float-end my-auto mt-1"
title="AutoRefresh"
></b-form-checkbox>
<b-button

View File

@ -7,7 +7,7 @@
:options="botStore.activeBot.strategyList"
>
</b-form-select>
<div class="ml-2">
<div class="ms-2">
<b-button @click="botStore.activeBot.getStrategyList">&#x21bb;</b-button>
</div>
</div>
@ -27,7 +27,7 @@ import { defineComponent, computed, onMounted } from 'vue';
export default defineComponent({
name: 'StrategySelect',
props: {
value: { type: String, required: true },
modelValue: { type: String, required: true },
showDetails: { default: false, required: false, type: Boolean },
},
emits: ['input'],
@ -37,7 +37,7 @@ export default defineComponent({
const strategyCode = computed((): string => botStore.activeBot.strategy?.code);
const locStrategy = computed({
get() {
return props.value;
return props.modelValue;
},
set(strategy: string) {
botStore.activeBot.getStrategy(strategy);

View File

@ -15,7 +15,7 @@
></b-form-input>
</b-input-group>
</b-form-group>
<b-form-group class="ml-2 col-md-6" label="End date" label-for="dp_dateTo">
<b-form-group class="ms-2 col-md-6" label="End date" label-for="dp_dateTo">
<b-input-group>
<b-input-group-prepend>
<b-form-datepicker v-model="dateTo" class="mb-1" button-only></b-form-datepicker>
@ -30,7 +30,7 @@
</b-input-group>
</b-form-group>
</div>
<label class="text-left">
<label class="text-start">
Timerange: <b>{{ timeRange }}</b>
</label>
</div>
@ -45,7 +45,7 @@ const now = new Date();
export default defineComponent({
name: 'TimeRangeSelect',
props: {
value: { required: true, type: String },
modelValue: { required: true, type: String },
},
setup(props, { emit }) {
const dateFrom = ref<string>('');
@ -63,7 +63,7 @@ export default defineComponent({
};
const updateInput = () => {
const tr = props.value.split('-');
const tr = props.modelValue.split('-');
if (tr[0]) {
dateFrom.value = timestampToDateString(dateFromString(tr[0], 'yyyyMMdd'));
}
@ -79,7 +79,7 @@ export default defineComponent({
);
onMounted(() => {
if (!props.value) {
if (!props.modelValue) {
dateFrom.value = timestampToDateString(new Date(now.getFullYear(), now.getMonth() - 1, 1));
} else {
updateInput();

View File

@ -2,48 +2,48 @@
<div class="d-flex flex-column">
<b-button
v-if="botApiVersion <= 1.1"
class="btn-xs text-left"
class="btn-xs text-start"
size="sm"
title="Forceexit"
@click="$emit('forceExit', trade)"
>
<ForceSellIcon :size="16" title="Forceexit" class="mr-1" />Forceexit
<ForceSellIcon :size="16" title="Forceexit" class="me-1" />Forceexit
</b-button>
<b-button
v-if="botApiVersion > 1.1"
class="btn-xs text-left"
class="btn-xs text-start"
size="sm"
title="Forceexit limit"
@click="$emit('forceExit', trade, 'limit')"
>
<ForceSellIcon :size="16" title="Forceexit limit" class="mr-1" />Forceexit limit
<ForceSellIcon :size="16" title="Forceexit limit" class="me-1" />Forceexit limit
</b-button>
<b-button
v-if="botApiVersion > 1.1"
class="btn-xs text-left mt-1"
class="btn-xs text-start mt-1"
size="sm"
title="Forceexit market"
@click="$emit('forceExit', trade, 'market')"
>
<ForceSellIcon :size="16" title="Forceexit market" class="mr-1" />Forceexit market
<ForceSellIcon :size="16" title="Forceexit market" class="me-1" />Forceexit market
</b-button>
<b-button
v-if="botApiVersion > 2.16"
class="btn-xs text-left mt-1"
class="btn-xs text-start mt-1"
size="sm"
title="Forceexit partial"
@click="$emit('forceExitPartial', trade)"
>
<ForceSellIcon :size="16" title="Forceexit partial" class="mr-1" />Forceexit partial
<ForceSellIcon :size="16" title="Forceexit partial" class="me-1" />Forceexit partial
</b-button>
<b-button
class="btn-xs text-left mt-1"
class="btn-xs text-start mt-1"
size="sm"
title="Delete trade"
@click="$emit('deleteTrade', trade)"
>
<DeleteIcon :size="16" title="Delete trade" class="mr-1" />
<DeleteIcon :size="16" title="Delete trade" class="me-1" />
Delete
</b-button>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div class="container text-left">
<div class="container text-start">
<div class="row">
<div class="col-lg-5">
<h5 class="detail-header">General</h5>
@ -25,7 +25,7 @@
v-if="trade.profit_ratio && trade.profit_abs"
:description="`${trade.is_open ? 'Current Profit' : 'Close Profit'}`"
>
<trade-profit class="ml-2" :trade="trade" />
<trade-profit class="ms-2" :trade="trade" />
</ValuePair>
<details>
<summary>Details</summary>
@ -97,7 +97,7 @@
:date="order.order_timestamp"
show-timezone
/>
<b class="ml-1">{{ order.ft_order_side }}</b> for
<b class="ms-1">{{ order.ft_order_side }}</b> for
<b>{{ formatPrice(order.safe_price) }}</b> |
<span v-if="order.remaining && order.remaining !== 0" title="remaining"
>{{ formatPrice(order.remaining, 8) }} /

View File

@ -5,7 +5,7 @@
small
hover
stacked="md"
:items="trades"
:items="[...trades]"
:fields="tableFields"
show-empty
:empty-text="emptyText"
@ -29,7 +29,12 @@
>
<ActionIcon :size="16" title="Actions" />
</b-button>
<b-popover :target="`btn-actions_${row.index}`" triggers="focus" placement="left">
<b-popover
:target="`btn-actions_${row.index}`"
:title="`Actions for ${row.item.pair}`"
triggers="focus"
placement="left"
>
<trade-actions
:trade="row.item"
:bot-api-version="botStore.activeBot.botApiVersion"
@ -83,7 +88,15 @@
style="width: unset"
/>
</div>
<force-exit-form v-if="activeTrades" :trade="feTrade" />
<force-exit-form v-if="activeTrades" v-model="forceExitVisible" :trade="feTrade" />
<b-modal
ref="removeTradeModal"
v-model="removeTradeVisible"
title="Exit trade"
@ok="forceExitExecuter"
>
{{ confirmExitText }}
</b-modal>
</div>
</template>
@ -97,7 +110,7 @@ import TradeProfit from './TradeProfit.vue';
import TradeActions from './TradeActions.vue';
import ForceExitForm from '@/components/ftbot/ForceExitForm.vue';
import { defineComponent, ref, computed, watch, getCurrentInstance, nextTick } from 'vue';
import { defineComponent, ref, computed, watch } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
@ -113,7 +126,6 @@ export default defineComponent({
emptyText: { default: 'No Trades to show.', type: String },
},
setup(props) {
const root = getCurrentInstance();
const botStore = useBotStore();
const currentPage = ref(1);
const selectedItemIndex = ref();
@ -121,6 +133,10 @@ export default defineComponent({
const feTrade = ref<Trade>({} as Trade);
const perPage = props.activeTrades ? 200 : 15;
const tradesTable = ref<HTMLFormElement>();
const forceExitVisible = ref(false);
const removeTradeVisible = ref(false);
const confirmExitText = ref('');
const removeTradeModal = ref<HTMLElement>();
const openFields: Record<string, string | Function>[] = [{ key: 'actions' }];
const closedFields: Record<string, string | Function>[] = [
@ -164,35 +180,51 @@ export default defineComponent({
{ key: 'open_timestamp', label: 'Open date' },
...(props.activeTrades ? openFields : closedFields),
];
const feOrderType = ref<string | undefined>(undefined);
const forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => {
root?.proxy.$bvModal
.msgBoxConfirm(
`Really exit trade ${item.trade_id} (Pair ${item.pair}) using ${ordertype} Order?`,
)
.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));
}
});
feTrade.value = item;
confirmExitText.value = `Really exit trade ${item.trade_id} (Pair ${item.pair}) using ${ordertype} Order?`;
removeTradeVisible.value = true;
feOrderType.value = ordertype;
};
const forceExitExecuter = () => {
// TODO: this should be done properly.
if (confirmExitText.value.includes('delete')) {
const payload: MultiDeletePayload = {
tradeid: String(feTrade.value.trade_id),
botId: feTrade.value.botId,
};
botStore.deleteTradeMulti(payload).catch((error) => console.log(error.response));
removeTradeVisible.value = false;
return;
}
console.log(confirmExitText.value);
const payload: MultiForcesellPayload = {
tradeid: String(feTrade.value.trade_id),
botId: feTrade.value.botId,
};
if (feOrderType.value) {
payload.ordertype = feOrderType.value;
}
botStore
.forceSellMulti(payload)
.then((xxx) => console.log(xxx))
.catch((error) => console.log(error.response));
feOrderType.value = undefined;
removeTradeVisible.value = false;
};
const removeTradeHandler = (item: Trade) => {
confirmExitText.value = `Really delete trade ${item.trade_id} (Pair ${item.pair})?`;
feTrade.value = item;
removeTradeVisible.value = true;
};
const forceExitPartialHandler = (item: Trade) => {
feTrade.value = item;
nextTick(() => {
console.log('showing modal', item);
root?.proxy.$bvModal.show('forceexit-modal');
});
forceExitVisible.value = true;
};
const handleContextMenuEvent = (item, index, event) => {
@ -205,31 +237,11 @@ export default defineComponent({
console.log(item);
};
const removeTradeHandler = (item) => {
console.log(item);
root?.proxy.$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) => {
const onRowClicked = (item) => {
// Only allow single selection mode!
if (
item &&
item.trade_id !== botStore.activeBot.detailTradeId &&
!tradesTable.value?.isRowSelected(index)
) {
if (item && item.trade_id !== botStore.activeBot.detailTradeId) {
botStore.activeBot.setDetailTrade(item);
} else {
console.log('unsetting item');
botStore.activeBot.setDetailTrade(null);
}
};
@ -275,6 +287,11 @@ export default defineComponent({
onRowClicked,
onRowSelected,
feTrade,
forceExitVisible,
removeTradeVisible,
confirmExitText,
removeTradeModal,
forceExitExecuter,
};
},
});

View File

@ -0,0 +1,42 @@
<template>
<b-modal
ref="removeTradeModal"
v-model="showRef"
:title="title"
@ok="msgBoxOK"
@cancel="showRef = false"
>
{{ message }}
</b-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
export interface MsgBoxObject {
title: string;
message: string;
accept: () => void;
}
const showRef = ref<boolean>(false);
const title = ref<string>('');
const message = ref<string>('');
const accept = ref<() => void>(() => {
console.warn('Accepted not set.');
});
const msgBoxOK = () => {
accept.value();
};
const show = (msg: MsgBoxObject) => {
title.value = msg.title;
message.value = msg.message;
showRef.value = true;
accept.value = msg.accept;
};
defineExpose({ show });
</script>
<style scoped></style>

View File

@ -1,12 +1,8 @@
import ProfitPill from './ProfitPill.vue';
import { createLocalVue } from '@vue/test-utils';
const localVue = createLocalVue();
describe('ProfitPill.vue', () => {
it('Shows a Green pill with positive profits', () => {
cy.mount(ProfitPill, {
localVue,
propsData: {
profitRatio: 0.051,
profitAbs: 0.1,
@ -21,7 +17,6 @@ describe('ProfitPill.vue', () => {
});
it('Shows a Red pill with positive profits', () => {
cy.mount(ProfitPill, {
localVue,
propsData: {
profitRatio: -0.1,
profitAbs: -0.1,
@ -38,7 +33,6 @@ describe('ProfitPill.vue', () => {
});
it('Shows a pill with 0.0 profits.', () => {
cy.mount(ProfitPill, {
localVue,
propsData: {
profitRatio: 0.0,
profitAbs: 0.0,
@ -54,7 +48,6 @@ describe('ProfitPill.vue', () => {
});
it('Shows a pill without relative profits.', () => {
cy.mount(ProfitPill, {
localVue,
propsData: {
profitRatio: undefined,
profitAbs: 223,

View File

@ -1,16 +1,16 @@
<template>
<div
class="d-flex justify-content-between align-items-center profit-pill pl-2 pr-1"
class="d-flex justify-content-between align-items-center profit-pill ps-2 pe-1"
:class="isProfitable ? 'profit-pill-profit' : ''"
:title="profitDesc"
>
<profit-symbol :profit="profitRatio || profitAbs" />
<profit-symbol :profit="(profitRatio || profitAbs) ?? 0" />
<div class="d-flex justify-content-center align-items-center flex-grow-1">
{{ profitRatio !== undefined ? formatPercent(profitRatio, 2) : '' }}
<span
v-if="profitString"
class="ml-1"
class="ms-1"
:class="profitRatio ? 'small' : ''"
:title="stakeCurrency"
>{{ profitString }}</span

View File

@ -1,17 +1,14 @@
import ProfitSymbol from '@/components/general/ProfitSymbol.vue';
import { createLocalVue } from '@vue/test-utils';
const localVue = createLocalVue();
describe('ProfitSymbol.vue', () => {
it('calculates isProfitable with negative profit', () => {
cy.mount(ProfitSymbol, { localVue, propsData: { profit: -0.5 } });
cy.mount(ProfitSymbol, { propsData: { profit: -0.5 } });
cy.get('div').should('have.class', 'triangle-down');
cy.get('div').should('not.have.class', 'triangle-up');
});
it('calculates isProfitable with positive profit', () => {
cy.mount(ProfitSymbol, { localVue, propsData: { profit: 0.5 } });
cy.mount(ProfitSymbol, { propsData: { profit: 0.5 } });
cy.get('div').should('have.class', 'triangle-up');
cy.get('div').should('not.have.class', 'triangle-down');
});

View File

@ -19,7 +19,7 @@ export default defineComponent({
},
classLabel: {
type: String,
default: 'col-4 font-weight-bold mb-0',
default: 'col-4 fw-bold mb-0',
},
classValue: {
type: String,

View File

@ -1,13 +1,13 @@
<template>
<header>
<b-navbar toggleable="sm" type="dark" variant="primary">
<b-navbar toggleable="sm" dark variant="primary">
<router-link class="navbar-brand" exact to="/">
<img class="logo" src="@/assets/freqtrade-logo.png" alt="Home Logo" />
<span class="navbar-brand-title d-sm-none d-md-inline">Freqtrade UI</span>
</router-link>
<!-- TODO: For XS breakpoint, this should be here... -->
<!-- <ReloadControl class="mr-3" /> -->
<!-- <ReloadControl class="me-3" /> -->
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" class="text-right text-md-center" is-nav>
@ -27,7 +27,7 @@
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto" menu-class="w-100">
<b-navbar-nav class="ms-auto" menu-class="w-100">
<!-- TODO This should show outside of the dropdown in XS mode -->
<div class="d-flex justify-content-between">
<b-dropdown
@ -44,10 +44,12 @@
</template>
<BotList :small="true" />
</b-dropdown>
<ReloadControl class="mr-3" />
<ReloadControl class="me-3" />
</div>
<li class="d-none d-sm-block nav-item text-secondary mr-2">
<b-nav-text class="verticalCenter small mr-2">
<li
class="d-none d-sm-flex flex-sm-wrap flex-lg-nowrap align-items-center nav-item text-secondary me-2"
>
<b-nav-text class="verticalCenter small me-2">
{{
(botStore.activeBotorUndefined && botStore.activeBotorUndefined.botName) ||
'No bot selected'
@ -67,11 +69,11 @@
<template #button-content>
<b-avatar size="2em" button>FT</b-avatar>
</template>
<b-dropdown-item>V: {{ settingsStore.uiVersion }}</b-dropdown-item>
<span class="ps-3">V: {{ settingsStore.uiVersion }}</span>
<router-link class="dropdown-item" to="/settings">Settings</router-link>
<b-form-checkbox v-model="layoutStore.layoutLocked" class="pl-5"
>Lock layout</b-form-checkbox
>
<div class="ps-3">
<b-form-checkbox v-model="layoutStore.layoutLocked">Lock layout</b-form-checkbox>
</div>
<b-dropdown-item @click="resetDynamicLayout">Reset Layout</b-dropdown-item>
<router-link
v-if="botStore.botCount === 1"
@ -83,9 +85,9 @@
</b-nav-item-dropdown>
<div class="d-block d-sm-none">
<!-- Visible only on XS -->
<li class="nav-item text-secondary ml-2 d-sm-none d-flex justify-content-between">
<li class="nav-item text-secondary ms-2 d-sm-none d-flex justify-content-between">
<div class="d-flex">
<b-nav-text class="verticalCenter small mr-2">
<b-nav-text class="verticalCenter small me-2">
{{
(botStore.activeBotorUndefined && botStore.activeBotorUndefined.botName) ||
'No bot selected'
@ -112,7 +114,7 @@
</li>
<li v-else>
<!-- should open Modal window! -->
<LoginModal />
<LoginModal v-if="route?.path !== '/login'" />
</li>
</b-navbar-nav>
</b-collapse>
@ -128,10 +130,10 @@ import ReloadControl from '@/components/ftbot/ReloadControl.vue';
import BotEntry from '@/components/BotEntry.vue';
import BotList from '@/components/BotList.vue';
import { defineComponent, ref, onBeforeUnmount, onMounted, watch } from 'vue';
import { useRoute } from '@/composables/router-helper';
import { OpenTradeVizOptions, useSettingsStore } from '@/stores/settings';
import { useLayoutStore } from '@/stores/layout';
import { useBotStore } from '@/stores/ftbotwrapper';
import { useRoute } from 'vue-router';
export default defineComponent({
name: 'NavBar',
@ -199,7 +201,6 @@ export default defineComponent({
onMounted(async () => {
await settingsStore.loadUIVersion();
pingInterval.value = window.setInterval(botStore.pingAll, 60000);
botStore.allRefreshFull();
});
settingsStore.$subscribe((_, state) => {
@ -234,6 +235,7 @@ export default defineComponent({
layoutStore,
botStore,
settingsStore,
route,
};
},
});
@ -253,17 +255,11 @@ export default defineComponent({
padding-left: 0.5em;
}
.navbar {
padding: 0.2rem 1rem;
padding: 0.2rem 0rem;
}
.router-link-active,
.nav-link:active {
color: white !important;
}
.verticalCenter {
align-items: center;
display: inline-flex;
height: 100%;
}
</style>

View File

@ -2,7 +2,7 @@
<footer class="d-md-none">
<!-- Only visible on xs (phone) viewport! -->
<hr class="my-0" />
<div class="d-flex flex-align-center justify-content-center">
<div class="d-flex flex-align-center justify-content-between px-2">
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/open_trades">
<OpenTradesIcon />
Trades

View File

@ -1,40 +0,0 @@
// TODO: This helper can be removed once
// vue-router either releases a new version, or we update to vue3.
import { effectScope, getCurrentInstance, reactive } from 'vue';
import { Route } from 'vue-router';
let currentRoute: Route;
function assign(target: Record<string, any>, source: Record<string, any>) {
for (const key of Object.keys(source)) {
target[key] = source[key];
}
return target;
}
export function useRoute(): Route {
const inst = getCurrentInstance();
if (!inst) {
return undefined as any;
}
if (!currentRoute) {
const scope = effectScope(true);
scope.run(() => {
const { $router } = inst.proxy;
currentRoute = reactive(assign({}, $router.currentRoute)) as any;
$router.afterEach((to) => {
assign(currentRoute, to);
});
});
}
return currentRoute;
}
export function useRouter() {
const inst = getCurrentInstance();
if (!inst) {
throw new Error('No current instance found');
}
const { proxy } = inst;
return proxy.$router;
}

View File

@ -1,20 +1,21 @@
import Vue from 'vue';
import './plugins/bootstrap-vue';
import { createApp } from 'vue';
import { BootstrapVue3 } from './plugins/bootstrap-vue';
import App from './App.vue';
import router from './router';
import { createPinia, PiniaVuePlugin } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import VueRouter from 'vue-router';
import GridLayout from 'vue3-drr-grid-layout';
Vue.use(PiniaVuePlugin);
const myApp = createApp(App);
myApp.use(PiniaVuePlugin);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
myApp.use(pinia);
Vue.use(VueRouter);
myApp.use(router);
myApp.use(BootstrapVue3);
myApp.use(GridLayout);
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
pinia,
}).$mount('#app');
// Vue.config.productionTip = false;
myApp.mount('#app');

View File

@ -1,7 +1,7 @@
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import BootstrapVue3 from 'bootstrap-vue-3';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue-3/dist/bootstrap-vue-3.css';
import '@/styles/main.scss';
Vue.use(BootstrapVue);
export { BootstrapVue3 };

View File

@ -1,7 +1,7 @@
import VueRouter, { RouteConfig } from 'vue-router';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { initBots, useBotStore } from '@/stores/ftbotwrapper';
const routes: Array<RouteConfig> = [
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
@ -68,15 +68,14 @@ const routes: Array<RouteConfig> = [
},
},
{
path: '*',
path: '/(.*)*',
name: '404',
component: () => import('@/views/Error404.vue'),
},
];
const router = new VueRouter({
mode: 'history',
base: import.meta.env.BASE_URL,
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

View File

@ -8,11 +8,13 @@ export function binData(data: number[], bins: number) {
Math.round((minimum + i * binSize) * 1000) / 1000,
0,
]);
// console.log(baseBins);
for (let i = 0; i < data.length; i++) {
const index = Math.min(Math.floor((data[i] - minimum) / binSize), bins - 1);
// console.log(data[i], index)
baseBins[index][1]++;
if (!isNaN(index)) {
baseBins[index][1]++;
}
}
return baseBins;

View File

@ -9,13 +9,14 @@ export const useAlertsStore = defineStore('alerts', {
addAlert(message: AlertType) {
this.activeMessages.push(message);
},
removeAlert() {
this.activeMessages.shift();
removeAlert(alert: AlertType) {
console.log('dismissed');
this.activeMessages = this.activeMessages.filter((v) => v !== alert);
},
},
});
export function showAlert(message: string, severity = '') {
const alertsStore = useAlertsStore();
alertsStore.addAlert({ message, severity });
alertsStore.addAlert({ message, severity, timeout: 5 });
}

View File

@ -115,6 +115,7 @@ export function createBotSubStore(botId: string, botName: string) {
);
},
tradeDetail: (state) => {
// console.log('tradeDetail', state.openTrades.length, state.openTrades);
let dTrade = state.openTrades.find((item) => item.trade_id === state.detailTradeId);
if (!dTrade) {
dTrade = state.trades.find((item) => item.trade_id === state.detailTradeId);

View File

@ -334,4 +334,5 @@ export function initBots() {
});
botStore.selectFirstBot();
botStore.startRefresh();
botStore.allRefreshFull();
}

View File

@ -1,22 +1,22 @@
import { GridItemData } from '@/types';
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',
multiPane = 0,
openTrades = 1,
tradeHistory = 2,
tradeDetail = 3,
chartView = 4,
}
export enum DashboardLayout {
dailyChart = 'g-dailyChart',
botComparison = 'g-botComparison',
allOpenTrades = 'g-allOpenTrades',
cumChartChart = 'g-cumChartChart',
allClosedTrades = 'g-allClosedTrades',
profitDistributionChart = 'g-profitDistributionChart',
tradesLogChart = 'g-TradesLogChart',
dailyChart = 0,
botComparison = 1,
allOpenTrades = 2,
cumChartChart = 3,
allClosedTrades = 4,
profitDistributionChart = 5,
tradesLogChart = 6,
}
// Define default layouts
@ -88,7 +88,7 @@ migrateLayoutSettings();
* @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 {
export function findGridLayout(gridLayout: GridItemData[], name: number): GridItemData {
let layout = gridLayout.find((value) => value.i === name);
if (!layout) {
layout = { i: name, x: 0, y: 0, w: 4, h: 6 };
@ -119,14 +119,24 @@ export const useLayoutStore = defineStore('layoutStore', {
persist: {
key: STORE_LAYOUTS,
afterRestore: (context) => {
console.log('after restore - ', context.store);
if (
context.store.dashboardLayout === null ||
typeof context.store.dashboardLayout === 'string'
typeof context.store.dashboardLayout === 'string' ||
context.store.dashboardLayout.length === 0 ||
typeof context.store.dashboardLayout[0]['i'] === 'string' ||
context.store.dashboardLayout.length < DEFAULT_DASHBOARD_LAYOUT.length
) {
console.log('loading dashboard Layout from default.');
context.store.dashboardLayout = JSON.parse(JSON.stringify(DEFAULT_DASHBOARD_LAYOUT));
}
if (context.store.tradingLayout === null || typeof context.store.tradingLayout === 'string') {
if (
context.store.tradingLayout === null ||
typeof context.store.tradingLayout === 'string' ||
context.store.tradingLayout.length === 0 ||
typeof context.store.tradingLayout[0]['i'] === 'string' ||
context.store.tradingLayout.length < DEFAULT_TRADING_LAYOUT.length
) {
console.log('loading trading Layout from default.');
context.store.tradingLayout = JSON.parse(JSON.stringify(DEFAULT_TRADING_LAYOUT));
}
},

View File

@ -7,6 +7,7 @@
.text-profit {
color: $color-profit;
}
.text-loss {
color: $color-loss;
}
@ -15,46 +16,62 @@
font-size: 0.8rem;
}
.modal.show {
// TODO: modal bootstrap fix (???)
display: block;
}
.btn-primary {
color: #ffffff
}
.text-bg-primary {
color: #ffffff !important;
}
[data-theme="dark"] {
$bg-dark: rgb(18, 18, 18);
$bg-darker: darken($bg-dark, 5%);
$fg-color: #dedede;
background-color: darken($bg-dark, 5%) ;
background-color: darken($bg-dark, 5%);
color: $fg-color !important;
body {
background: $bg-darker;
color: $fg-color;
}
.card {
border-color: lighten($bg-dark, 10%);
background-color: $bg-dark;
}
.card-body {
background: $bg-dark;
color: $fg-color;
}
.card-header {
background: lighten($bg-dark, 5%);
color: $fg-color;
}
.table {
color: $fg-color;
}
.list-group-item {
color: $fg-color;
background: $bg-dark;
}
// .custom-select {
// color: $fg-color;
// // background: $bg-dark;
// }
.nav-tabs {
border-bottom: 1px solid lighten($bg-dark, 20%);
.nav-link
{
.nav-link {
color: $primary;
&:hover {
@ -63,26 +80,32 @@
}
}
}
.nav-link.active {
color: $fg-color;
background: $bg-darker;
border-color: lighten($bg-dark, 20%);
border-color: lighten($bg-dark, 20%);
}
.modal-content {
color: $fg-color;
background: $bg-dark;
}
.form-control {
color: $fg-color;
background: $bg-dark;
}
.popover {
background: $bg-dark;
border-color: lighten($bg-dark, 20%);
border-color: lighten($bg-dark, 20%);
}
.popover-header {
background: $bg-darker;
}
.popover-body {
color: $fg-color;
}
@ -90,26 +113,31 @@
.logo-svg {
background-color: $fg-color;
}
textarea {
color: $fg-color;
background: $bg-dark;
}
.text-dark {
color: $fg-color !important;
}
.dropdown-menu
{
.dropdown-menu {
background-color: lighten($bg-dark, 5%);
color: $fg-color;
}
.dropdown-item {
color: $fg-color;
}
// Styles for searchable select
.vs__dropdown-toggle{
.vs__dropdown-toggle {
border-color: lighten($bg-dark, 20%);
// border: 1px solid $fg-color;
}
.style-chooser .vs__search::placeholder,
.style-chooser .vs__dropdown-toggle,
.style-chooser .vs__dropdown-menu,
@ -118,47 +146,69 @@
color: $fg-color;
}
.vs__search, .vs__dropdown-option {
.vs__search,
.vs__dropdown-option {
color: $fg-color;
}
.vs__open-indicator {
fill: darken($fg-color, 10%);
}
.vs__selected {
color: $fg-color;
}
hr {
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.table.b-table > tbody > .table-active, .table.b-table > tbody > .table-active > th, .table.b-table > tbody > .table-active > td {
// Table selected cells
background: darken($bg-dark, 10%);
.table {
color: $fg-color;
}
.table-hover tbody tr:hover td, .table-hover tbody tr:hover th,
.table.b-table.table-hover > tbody > tr.table-active:hover td, .table.b-table.table-hover > tbody > tr.table-active:hover th{
.table.b-table>tbody>.selected,
.table.b-table>tbody>.selected>th,
.table.b-table>tbody>.selected>td {
// Table selected cells
color: $fg-color;
background: darken($bg-dark, 10%);
}
.table-hover tbody tr:hover td,
.table-hover tbody tr:hover th,
.table.b-table>tbody>.selected:hover,
.table.b-table.table-hover>tbody>tr.selected th:hover {
// Table selected cells hover
color: $fg-color;
background: lighten($bg-dark, 10%);
--bs-table-hover-bg: #272727;
}
.custom-select {
.form-select {
color: $fg-color;
border-color: lighten($bg-dark, 20%);
background: $bg-dark url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23dedede' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px
}
.b-toast .toast {
background: $bg-dark;
border-color: lighten($bg-dark, 20%);
}
.toast-header {
color: $fg-color;
background: darken($bg-dark, 10%);
border-color: lighten($bg-dark, 20%);
}
}
html.ft-theme-transition,
html.ft-theme-transition *,
html.ft-theme-transition *:before,
html.ft-theme-transition *:after {
transition:background 750ms ease-in-out,
border-color 750ms ease-in-out;
transition: background 750ms ease-in-out,
border-color 750ms ease-in-out;
transition-delay: 0 !important;
}

View File

@ -2,8 +2,9 @@
@import 'bootstrap_variables_ovw';
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-vue/src/index';
// @import 'bootstrap-vue/src/index';
@import "vue-select/src/scss/vue-select.scss";
// @import "vue-select/src/scss/vue-select.scss";
@import 'vue-select/dist/vue-select.css';
@import 'variables';
@import 'styles_ovw';

View File

@ -1,4 +1,5 @@
export interface AlertType {
message: string;
severity: string;
timeout: number;
}

16
src/types/gridLayout.ts Normal file
View File

@ -0,0 +1,16 @@
// TODO: This interface should really come from the grid-layout package
export interface GridItemData {
x: number;
y: number;
w: number;
h: number;
i: number;
isDraggable?: boolean;
isResizable?: boolean;
maxH?: number;
maxW?: number;
minH?: number;
minW?: number;
moved?: boolean;
static?: boolean;
}

View File

@ -10,3 +10,4 @@ export * from './plot';
export * from './profit';
export * from './trades';
export * from './types';
export * from './gridLayout';

View File

@ -60,7 +60,7 @@
</div>
<small
v-show="botStore.activeBot.backtestRunning"
class="text-right bt-running-label col-8 col-lg-3"
class="text-end bt-running-label col-8 col-lg-3"
>Backtest running: {{ botStore.activeBot.backtestStep }}
{{ formatPercent(botStore.activeBot.backtestProgress, 2) }}</small
>
@ -70,7 +70,7 @@
<div class="d-md-flex">
<!-- Left bar -->
<div
:class="`${showLeftBar ? 'col-md-3' : ''} sticky-top sticky-offset mr-3 d-flex flex-column`"
:class="`${showLeftBar ? 'col-md-3' : ''} sticky-top sticky-offset me-3 d-flex flex-column`"
>
<b-button
v-if="btFormMode !== 'visualize'"
@ -107,7 +107,7 @@
label-cols-lg="2"
label="Backtest params"
label-size="sm"
label-class="font-weight-bold pt-0"
label-class="fw-bold pt-0"
class="mb-0"
>
<b-form-group

View File

@ -1,8 +1,8 @@
<template>
<GridLayout
<grid-layout
class="h-100 w-100"
:row-height="50"
:layout="gridLayout"
:layout="gridLayoutData"
:vertical-compact="false"
:margin="[5, 5]"
:responsive-layouts="responsiveGridLayouts"
@ -11,124 +11,132 @@
:responsive="true"
:prevent-collision="true"
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
:col-num="12"
@layout-updated="layoutUpdatedEvent"
@breakpoint-changed="breakpointChanged"
>
<GridItem
:i="gridLayoutDaily.i"
:x="gridLayoutDaily.x"
:y="gridLayoutDaily.y"
:w="gridLayoutDaily.w"
:h="gridLayoutDaily.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer :header="`Daily Profit ${botStore.botCount > 1 ? 'combined' : ''}`">
<DailyChart
v-if="botStore.allDailyStatsSelectedBots"
:daily-stats="botStore.allDailyStatsSelectedBots"
:show-title="false"
/>
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutBotComparison.i"
:x="gridLayoutBotComparison.x"
:y="gridLayoutBotComparison.y"
:w="gridLayoutBotComparison.w"
:h="gridLayoutBotComparison.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Bot comparison">
<bot-comparison-list />
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutAllOpenTrades.i"
:x="gridLayoutAllOpenTrades.x"
:y="gridLayoutAllOpenTrades.y"
:w="gridLayoutAllOpenTrades.w"
:h="gridLayoutAllOpenTrades.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Open Trades">
<trade-list active-trades :trades="botStore.allOpenTradesSelectedBots" multi-bot-view />
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutCumChart.i"
:x="gridLayoutCumChart.x"
:y="gridLayoutCumChart.y"
:w="gridLayoutCumChart.w"
:h="gridLayoutCumChart.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Cumulative Profit">
<CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutAllClosedTrades.i"
:x="gridLayoutAllClosedTrades.x"
:y="gridLayoutAllClosedTrades.y"
:w="gridLayoutAllClosedTrades.w"
:h="gridLayoutAllClosedTrades.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Closed Trades">
<trade-list
:active-trades="false"
show-filter
:trades="botStore.allClosedTradesSelectedBots"
multi-bot-view
/>
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutProfitDistribution.i"
:x="gridLayoutProfitDistribution.x"
:y="gridLayoutProfitDistribution.y"
:w="gridLayoutProfitDistribution.w"
:h="gridLayoutProfitDistribution.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Profit Distribution">
<ProfitDistributionChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutTradesLogChart.i"
:x="gridLayoutTradesLogChart.x"
:y="gridLayoutTradesLogChart.y"
:w="gridLayoutTradesLogChart.w"
:h="gridLayoutTradesLogChart.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Trades Log">
<TradesLogChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</GridItem>
</GridLayout>
<template #default="{ gridItemProps }">
<grid-item
v-bind="gridItemProps"
:i="gridLayoutDaily.i"
:x="gridLayoutDaily.x"
:y="gridLayoutDaily.y"
:w="gridLayoutDaily.w"
:h="gridLayoutDaily.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer :header="`Daily Profit ${botStore.botCount > 1 ? 'combined' : ''}`">
<DailyChart
v-if="botStore.allDailyStatsSelectedBots"
:daily-stats="botStore.allDailyStatsSelectedBots"
:show-title="false"
/>
</DraggableContainer>
</grid-item>
<grid-item
v-bind="gridItemProps"
:i="gridLayoutBotComparison.i"
:x="gridLayoutBotComparison.x"
:y="gridLayoutBotComparison.y"
:w="gridLayoutBotComparison.w"
:h="gridLayoutBotComparison.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Bot comparison">
<bot-comparison-list />
</DraggableContainer>
</grid-item>
<grid-item
v-bind="gridItemProps"
:i="gridLayoutAllOpenTrades.i"
:x="gridLayoutAllOpenTrades.x"
:y="gridLayoutAllOpenTrades.y"
:w="gridLayoutAllOpenTrades.w"
:h="gridLayoutAllOpenTrades.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Open Trades">
<trade-list active-trades :trades="botStore.allOpenTradesSelectedBots" multi-bot-view />
</DraggableContainer>
</grid-item>
<grid-item
v-bind="gridItemProps"
:i="gridLayoutCumChart.i"
:x="gridLayoutCumChart.x"
:y="gridLayoutCumChart.y"
:w="gridLayoutCumChart.w"
:h="gridLayoutCumChart.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Cumulative Profit">
<CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</grid-item>
<grid-item
v-bind="gridItemProps"
:i="gridLayoutAllClosedTrades.i"
:x="gridLayoutAllClosedTrades.x"
:y="gridLayoutAllClosedTrades.y"
:w="gridLayoutAllClosedTrades.w"
:h="gridLayoutAllClosedTrades.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Closed Trades">
<trade-list
:active-trades="false"
show-filter
:trades="botStore.allClosedTradesSelectedBots"
multi-bot-view
/>
</DraggableContainer>
</grid-item>
<grid-item
v-bind="gridItemProps"
:i="gridLayoutProfitDistribution.i"
:x="gridLayoutProfitDistribution.x"
:y="gridLayoutProfitDistribution.y"
:w="gridLayoutProfitDistribution.w"
:h="gridLayoutProfitDistribution.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Profit Distribution">
<ProfitDistributionChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</grid-item>
<grid-item
v-bind="gridItemProps"
:i="gridLayoutTradesLogChart.i"
:x="gridLayoutTradesLogChart.x"
:y="gridLayoutTradesLogChart.y"
:w="gridLayoutTradesLogChart.w"
:h="gridLayoutTradesLogChart.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Trades Log">
<TradesLogChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</grid-item>
</template>
</grid-layout>
</template>
<script lang="ts">
import { formatPrice } from '@/shared/formatters';
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
import DailyChart from '@/components/charts/DailyChart.vue';
import CumProfitChart from '@/components/charts/CumProfitChart.vue';
import TradesLogChart from '@/components/charts/TradesLog.vue';
@ -140,12 +148,11 @@ import DraggableContainer from '@/components/layout/DraggableContainer.vue';
import { defineComponent, ref, computed, onMounted } from 'vue';
import { DashboardLayout, findGridLayout, useLayoutStore } from '@/stores/layout';
import { useBotStore } from '@/stores/ftbotwrapper';
import { GridItemData } from '@/types';
export default defineComponent({
name: 'Dashboard',
components: {
GridLayout,
GridItem,
DailyChart,
CumProfitChart,
ProfitDistributionChart,
@ -171,7 +178,7 @@ export default defineComponent({
return layoutStore.layoutLocked || !isResizableLayout;
});
const gridLayout = computed((): GridItemData[] => {
const gridLayoutData = computed((): GridItemData[] => {
if (isResizableLayout) {
return layoutStore.dashboardLayout;
}
@ -187,28 +194,28 @@ export default defineComponent({
};
const gridLayoutDaily = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.dailyChart);
return findGridLayout(gridLayoutData.value, DashboardLayout.dailyChart);
});
const gridLayoutBotComparison = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.botComparison);
return findGridLayout(gridLayoutData.value, DashboardLayout.botComparison);
});
const gridLayoutAllOpenTrades = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.allOpenTrades);
return findGridLayout(gridLayoutData.value, DashboardLayout.allOpenTrades);
});
const gridLayoutAllClosedTrades = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.allClosedTrades);
return findGridLayout(gridLayoutData.value, DashboardLayout.allClosedTrades);
});
const gridLayoutCumChart = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.cumChartChart);
return findGridLayout(gridLayoutData.value, DashboardLayout.cumChartChart);
});
const gridLayoutProfitDistribution = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.profitDistributionChart);
return findGridLayout(gridLayoutData.value, DashboardLayout.profitDistributionChart);
});
const gridLayoutTradesLogChart = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.tradesLogChart);
return findGridLayout(gridLayoutData.value, DashboardLayout.tradesLogChart);
});
const responsiveGridLayouts = computed(() => {
@ -230,7 +237,7 @@ export default defineComponent({
isLayoutLocked,
layoutUpdatedEvent,
breakpointChanged,
gridLayout,
gridLayoutData,
gridLayoutDaily,
gridLayoutBotComparison,
gridLayoutAllOpenTrades,

View File

@ -1,16 +1,16 @@
<template>
<div class="d-flex flex-column h-100">
<!-- <div v-if="isWebserverMode" class="mr-auto ml-3"> -->
<!-- <div v-if="isWebserverMode" class="me-auto ms-3"> -->
<!-- Currently only available in Webserver mode -->
<!-- <b-form-checkbox v-model="historicView">HistoricData</b-form-checkbox> -->
<!-- </div> -->
<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">
<div class="col-md-3 text-start">
<span>Strategy</span>
<StrategySelect v-model="strategy" class="mt-1"></StrategySelect>
</div>
<div class="col-md-3 text-left">
<div class="col-md-3 text-start">
<span>Timeframe</span>
<TimeframeSelect v-model="selectedTimeframe" class="mt-1" />
</div>

View File

@ -1,6 +1,6 @@
<template>
<div class="home">
<div class="container col-12 col-sm-6 col-lg-4">
<div class="d-flex justify-content-center">
<bot-list />
</div>
<hr />

View File

@ -1,7 +1,12 @@
<template>
<div>
<b-button v-b-modal.modal-prevent-closing>{{ loginText }}</b-button>
<b-modal id="modal-prevent-closing" ref="modalRef" title="Login to your bot" @ok="handleOk">
<b-button @click="loginViewOpen = true">{{ loginText }}</b-button>
<b-modal
id="modal-prevent-closing"
v-model="loginViewOpen"
title="Login to your bot"
@ok="handleOk"
>
<Login ref="loginForm" in-modal @loginResult="handleLoginResult" />
</b-modal>
</div>
@ -12,10 +17,6 @@ import { defineComponent, ref } from 'vue';
import Login from '@/components/Login.vue';
interface HTMLModal extends HTMLElement {
hide: () => void;
}
export default defineComponent({
name: 'LoginModal',
components: { Login },
@ -23,19 +24,18 @@ export default defineComponent({
loginText: { required: false, default: 'Login', type: String },
},
setup() {
const modalRef = ref<HTMLModal>();
const loginViewOpen = ref(false);
const loginForm = ref<HTMLFormElement>();
const handleLoginResult = (result: boolean) => {
if (result) {
modalRef.value?.hide();
loginViewOpen.value = false;
}
};
const handleOk = (evt) => {
evt.preventDefault();
const handleOk = () => {
loginForm.value?.handleSubmit();
};
return {
modalRef,
loginViewOpen,
loginForm,
handleOk,
handleLoginResult,

View File

@ -1,7 +1,7 @@
<template>
<div class="container mt-3">
<b-card header="FreqUI Settings">
<div class="text-left">
<div class="text-start">
<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."

View File

@ -25,7 +25,7 @@
<div v-if="botStore.activeBot.detailTradeId" class="d-flex flex-column">
<b-button
size="sm"
class="align-self-start mt-1 ml-1"
class="align-self-start mt-1 ms-1"
@click="botStore.activeBot.setDetailTrade(null)"
><BackIcon /> Back</b-button
>

View File

@ -1,8 +1,8 @@
<template>
<GridLayout
<grid-layout
class="h-100 w-100"
:row-height="50"
:layout="gridLayout"
:layout="gridLayoutData"
:vertical-compact="false"
:margin="[5, 5]"
:responsive-layouts="responsiveGridLayouts"
@ -10,132 +10,140 @@
:is-draggable="!isLayoutLocked"
:responsive="true"
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
@layout-updated="layoutUpdatedEvent"
@breakpoint-changed="breakpointChanged"
:col-num="12"
@update:layout="layoutUpdatedEvent"
@update:breakpoint="breakpointChanged"
>
<GridItem
v-if="gridLayoutMultiPane.h != 0"
:i="gridLayoutMultiPane.i"
:x="gridLayoutMultiPane.x"
:y="gridLayoutMultiPane.y"
:w="gridLayoutMultiPane.w"
:h="gridLayoutMultiPane.h"
drag-allow-from=".card-header"
>
<DraggableContainer header="Multi Pane">
<div class="mt-1 d-flex justify-content-center">
<BotControls class="mt-1 mb-2" />
</div>
<b-tabs content-class="mt-3" class="mt-1">
<b-tab title="Pairs combined" active>
<PairSummary
:pairlist="botStore.activeBot.whitelist"
:current-locks="botStore.activeBot.activeLocks"
:trades="botStore.activeBot.openTrades"
/>
</b-tab>
<b-tab title="General">
<BotStatus />
</b-tab>
<b-tab title="Performance">
<Performance class="performance-view" />
</b-tab>
<b-tab title="Balance" lazy>
<Balance />
</b-tab>
<b-tab title="Daily Stats" lazy>
<DailyStats />
</b-tab>
<template #default="{ gridItemProps }">
<grid-item
v-if="gridLayoutMultiPane.h != 0"
v-bind="gridItemProps"
:i="gridLayoutMultiPane.i"
:x="gridLayoutMultiPane.x"
:y="gridLayoutMultiPane.y"
:w="gridLayoutMultiPane.w"
:h="gridLayoutMultiPane.h"
drag-allow-from=".card-header"
>
<DraggableContainer header="Multi Pane">
<div class="mt-1 d-flex justify-content-center">
<BotControls class="mt-1 mb-2" />
</div>
<b-tabs content-class="mt-3" class="mt-1">
<b-tab title="Pairs combined" active>
<PairSummary
:pairlist="botStore.activeBot.whitelist"
:current-locks="botStore.activeBot.activeLocks"
:trades="botStore.activeBot.openTrades"
/>
</b-tab>
<b-tab title="General">
<BotStatus />
</b-tab>
<b-tab title="Performance">
<Performance class="performance-view" />
</b-tab>
<b-tab title="Balance" lazy>
<Balance />
</b-tab>
<b-tab title="Daily Stats" lazy>
<DailyStats />
</b-tab>
<b-tab title="Pairlist" lazy>
<FTBotAPIPairList />
</b-tab>
<b-tab title="Pair Locks" lazy>
<PairLockList />
</b-tab>
</b-tabs>
</DraggableContainer>
</GridItem>
<GridItem
v-if="gridLayoutOpenTrades.h != 0"
:i="gridLayoutOpenTrades.i"
:x="gridLayoutOpenTrades.x"
:y="gridLayoutOpenTrades.y"
:w="gridLayoutOpenTrades.w"
:h="gridLayoutOpenTrades.h"
drag-allow-from=".card-header"
>
<DraggableContainer header="Open Trades">
<TradeList
class="open-trades"
:trades="botStore.activeBot.openTrades"
title="Open trades"
:active-trades="true"
empty-text="Currently no open trades."
/>
</DraggableContainer>
</GridItem>
<GridItem
v-if="gridLayoutTradeHistory.h != 0"
:i="gridLayoutTradeHistory.i"
:x="gridLayoutTradeHistory.x"
:y="gridLayoutTradeHistory.y"
:w="gridLayoutTradeHistory.w"
:h="gridLayoutTradeHistory.h"
drag-allow-from=".card-header"
>
<DraggableContainer header="Closed Trades">
<trade-list
class="trade-history"
:trades="botStore.activeBot.closedTrades"
title="Trade history"
:show-filter="true"
empty-text="No closed trades so far."
/>
</DraggableContainer>
</GridItem>
<GridItem
v-if="botStore.activeBot.detailTradeId && gridLayoutTradeDetail.h != 0"
:i="gridLayoutTradeDetail.i"
:x="gridLayoutTradeDetail.x"
:y="gridLayoutTradeDetail.y"
:w="gridLayoutTradeDetail.w"
:h="gridLayoutTradeDetail.h"
:min-h="4"
drag-allow-from=".card-header"
>
<DraggableContainer header="Trade Detail">
<TradeDetail
:trade="botStore.activeBot.tradeDetail"
:stake-currency="botStore.activeBot.stakeCurrency"
/>
</DraggableContainer>
</GridItem>
<GridItem
v-if="gridLayoutTradeDetail.h != 0"
:i="gridLayoutChartView.i"
:x="gridLayoutChartView.x"
:y="gridLayoutChartView.y"
:w="gridLayoutChartView.w"
:h="gridLayoutChartView.h"
:min-h="6"
drag-allow-from=".card-header"
>
<DraggableContainer header="Chart">
<CandleChartContainer
:available-pairs="botStore.activeBot.whitelist"
:historic-view="!!false"
:timeframe="botStore.activeBot.timeframe"
:trades="botStore.activeBot.allTrades"
>
</CandleChartContainer>
</DraggableContainer>
</GridItem>
</GridLayout>
<b-tab title="Pairlist" lazy>
<FTBotAPIPairList />
</b-tab>
<b-tab title="Pair Locks" lazy>
<PairLockList />
</b-tab>
</b-tabs>
</DraggableContainer>
</grid-item>
<grid-item
v-if="gridLayoutOpenTrades.h != 0"
v-bind="gridItemProps"
:i="gridLayoutOpenTrades.i"
:x="gridLayoutOpenTrades.x"
:y="gridLayoutOpenTrades.y"
:w="gridLayoutOpenTrades.w"
:h="gridLayoutOpenTrades.h"
drag-allow-from=".card-header"
>
<DraggableContainer header="Open Trades">
<TradeList
class="open-trades"
:trades="botStore.activeBot.openTrades"
title="Open trades"
:active-trades="true"
empty-text="Currently no open trades."
/>
</DraggableContainer>
</grid-item>
<grid-item
v-if="gridLayoutTradeHistory.h != 0"
v-bind="gridItemProps"
:i="gridLayoutTradeHistory.i"
:x="gridLayoutTradeHistory.x"
:y="gridLayoutTradeHistory.y"
:w="gridLayoutTradeHistory.w"
:h="gridLayoutTradeHistory.h"
drag-allow-from=".card-header"
>
<DraggableContainer header="Closed Trades">
<trade-list
class="trade-history"
:trades="botStore.activeBot.closedTrades"
title="Trade history"
:show-filter="true"
empty-text="No closed trades so far."
/>
</DraggableContainer>
</grid-item>
<grid-item
v-if="botStore.activeBot.detailTradeId && gridLayoutTradeDetail.h != 0"
v-bind="gridItemProps"
:i="gridLayoutTradeDetail.i"
:x="gridLayoutTradeDetail.x"
:y="gridLayoutTradeDetail.y"
:w="gridLayoutTradeDetail.w"
:h="gridLayoutTradeDetail.h"
:min-h="4"
drag-allow-from=".card-header"
>
<DraggableContainer header="Trade Detail">
<TradeDetail
:trade="botStore.activeBot.tradeDetail"
:stake-currency="botStore.activeBot.stakeCurrency"
/>
</DraggableContainer>
</grid-item>
<grid-item
v-if="gridLayoutTradeDetail.h != 0"
v-bind="gridItemProps"
:i="gridLayoutChartView.i"
:x="gridLayoutChartView.x"
:y="gridLayoutChartView.y"
:w="gridLayoutChartView.w"
:h="gridLayoutChartView.h"
:min-h="6"
drag-allow-from=".card-header"
>
<DraggableContainer header="Chart">
<CandleChartContainer
:available-pairs="botStore.activeBot.whitelist"
:historic-view="!!false"
:timeframe="botStore.activeBot.timeframe"
:trades="botStore.activeBot.allTrades"
>
</CandleChartContainer>
</DraggableContainer>
</grid-item>
</template>
</grid-layout>
</template>
<script lang="ts">
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
import { GridItemData } from '@/types';
import Balance from '@/components/ftbot/Balance.vue';
import BotControls from '@/components/ftbot/BotControls.vue';
@ -164,8 +172,6 @@ export default defineComponent({
DailyStats,
DraggableContainer,
FTBotAPIPairList,
GridItem,
GridLayout,
PairLockList,
PairSummary,
Performance,
@ -187,7 +193,7 @@ export default defineComponent({
const isLayoutLocked = computed(() => {
return layoutStore.layoutLocked || !isResizableLayout;
});
const gridLayout = computed((): GridItemData[] => {
const gridLayoutData = computed((): GridItemData[] => {
if (isResizableLayout) {
return layoutStore.tradingLayout;
}
@ -195,23 +201,23 @@ export default defineComponent({
});
const gridLayoutMultiPane = computed(() => {
return findGridLayout(gridLayout.value, TradeLayout.multiPane);
return findGridLayout(gridLayoutData.value, TradeLayout.multiPane);
});
const gridLayoutOpenTrades = computed(() => {
return findGridLayout(gridLayout.value, TradeLayout.openTrades);
return findGridLayout(gridLayoutData.value, TradeLayout.openTrades);
});
const gridLayoutTradeHistory = computed(() => {
return findGridLayout(gridLayout.value, TradeLayout.tradeHistory);
return findGridLayout(gridLayoutData.value, TradeLayout.tradeHistory);
});
const gridLayoutTradeDetail = computed(() => {
return findGridLayout(gridLayout.value, TradeLayout.tradeDetail);
return findGridLayout(gridLayoutData.value, TradeLayout.tradeDetail);
});
const gridLayoutChartView = computed(() => {
return findGridLayout(gridLayout.value, TradeLayout.chartView);
return findGridLayout(gridLayoutData.value, TradeLayout.chartView);
});
const responsiveGridLayouts = computed(() => {
@ -233,7 +239,7 @@ export default defineComponent({
breakpointChanged,
layoutUpdatedEvent,
isLayoutLocked,
gridLayout,
gridLayoutData,
gridLayoutMultiPane,
gridLayoutOpenTrades,
gridLayoutTradeHistory,

View File

@ -35,10 +35,10 @@
],
"vueCompilerOptions": {
"experimentalImplicitWrapComponentOptionsWithDefineComponent": false,
"target": 2.7,
"target": 3,
"experimentalTemplateCompilerOptions": {
"compatConfig": {
"MODE": 2
"MODE": 3
} // optional
},
"experimentalModelPropName": {

View File

@ -1,17 +1,15 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue2';
import createVuePlugin from '@vitejs/plugin-vue';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
plugins: [createVuePlugin({})],
resolve: {
alias: [
{
find: '@',
replacement: resolve(__dirname, 'src'),
},
],
dedupe: ['vue'],
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
chunkSizeWarningLimit: 700, // Default is 500

1720
yarn.lock

File diff suppressed because it is too large Load Diff