Merge branch 'main' into feat/pairlistconfig

This commit is contained in:
Tako 2023-05-23 18:34:57 +00:00
commit 36737ae6dc
90 changed files with 15527 additions and 3068 deletions

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:18-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bullseye
RUN sudo apt-get update \
&& sudo apt-get install -y vim \

View File

@ -1,35 +1,41 @@
/* cSpell:disable */
{
"name": "frequi",
"build": {
"dockerfile": "Dockerfile"
},
"forwardPorts": [
3000
],
"mounts": [
"source=frequi-bashhistory,target=/home/node/commandhistory,type=volume"
],
"remoteUser": "node",
"settings": {
// "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"emmet.includeLanguages": {
"vue": "html",
"vue-html": "html"
},
"workbench.iconTheme": "vscode-icons",
},
"extensions": [
"vue.volar",
"dbaeumer.vscode-eslint",
"yzhang.markdown-all-in-one",
"marquesmps.dockerfile-validator",
"streetsidesoftware.code-spell-checker",
"vscode-icons-team.vscode-icons",
"hediet.vscode-drawio",
],
"postCreateCommand": "yarn install",
"name": "frequi",
"build": {
"dockerfile": "Dockerfile"
},
"forwardPorts": [
3000
],
"mounts": [
"source=frequi-bashhistory,target=/home/node/commandhistory,type=volume"
],
"remoteUser": "node",
"customizations": {
"vscode": {
"settings": {
// "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"emmet.includeLanguages": {
"vue": "html",
"vue-html": "html"
},
"workbench.iconTheme": "vscode-icons"
},
"extensions": [
"vue.volar",
"dbaeumer.vscode-eslint",
"yzhang.markdown-all-in-one",
"marquesmps.dockerfile-validator",
"streetsidesoftware.code-spell-checker",
"vscode-icons-team.vscode-icons",
"hediet.vscode-drawio",
"ZixuanChen.vitest-explorer",
"antfu.iconify"
]
}
},
"postCreateCommand": "yarn install",
}

View File

@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-22.04 ]
node: [ "16", "18", "19"]
node: [ "16", "18", "19", "20"]
steps:
- uses: actions/checkout@v3

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
components.d.ts

View File

@ -1,4 +1,4 @@
FROM node:19.9.0-alpine as ui-builder
FROM node:20.2.0-alpine as ui-builder
RUN mkdir /app

View File

@ -24,4 +24,8 @@ export function defaultMocks() {
cy.intercept('GET', '**/api/v1/show_config', {
fixture: 'show_config.json',
}).as('ShowConf');
cy.intercept('GET', '**/api/v1/pair_candles?*', {
fixture: 'pair_candles_btc_1m.json',
}).as('PairCandles');
}

File diff suppressed because it is too large Load Diff

View File

@ -17,53 +17,55 @@
},
"dependencies": {
"@popperjs/core": "^2.11.7",
"@vuepic/vue-datepicker": "^4.4.0",
"@vueuse/core": "^10.0.2",
"@vueuse/integrations": "^10.0.2",
"axios": "^1.3.5",
"@vuepic/vue-datepicker": "^5.1.1",
"@vueuse/core": "^10.1.2",
"@vueuse/integrations": "^10.1.2",
"axios": "^1.4.0",
"bootstrap": "^5.2.3",
"bootstrap-vue-next": "^0.8.5",
"core-js": "^3.30.1",
"date-fns": "^2.29.3",
"bootstrap-vue-next": "^0.8.11",
"core-js": "^3.30.2",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"echarts": "^5.4.2",
"favico.js": "^0.3.10",
"humanize-duration": "^3.28.0",
"pinia": "^2.0.34",
"pinia": "^2.0.36",
"pinia-plugin-persistedstate": "^3.1.0",
"sortablejs": "^1.15.0",
"vue": "^3.2.47",
"vue": "^3.3.2",
"vue-class-component": "^7.2.5",
"vue-demi": "^0.14.0",
"vue-echarts": "^6.5.4",
"vue-material-design-icons": "^5.2.0",
"vue-router": "^4.1.6",
"vue-demi": "^0.14.1",
"vue-echarts": "^6.5.5",
"vue-router": "^4.2.0",
"vue-select": "^4.0.0-beta.6",
"vue3-drr-grid-layout": "^1.9.7"
},
"devDependencies": {
"@cypress/vite-dev-server": "^5.0.5",
"@cypress/vue": "^5.0.5",
"@iconify-json/mdi": "^1.1.52",
"@types/echarts": "^4.9.17",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.58.0",
"@vitejs/plugin-vue": "^4.1.0",
"@vue/compiler-sfc": "3.2.47",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "3.3.2",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/runtime-dom": "^3.2.47",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-dom": "^3.3.2",
"@vue/test-utils": "^2.3.2",
"cypress": "^12.10.0",
"eslint": "^8.38.0",
"cypress": "^12.12.0",
"eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.11.0",
"eslint-plugin-vue": "^9.13.0",
"mutationobserver-shim": "^0.3.7",
"portal-vue": "^3.0.0",
"prettier": "^2.8.7",
"sass": "^1.62.0",
"prettier": "^2.8.8",
"sass": "^1.62.1",
"typescript": "~5.0.4",
"vite": "^4.2.2",
"vitest": "^0.30.1",
"vue-tsc": "^1.2.0"
"unplugin-icons": "^0.16.1",
"unplugin-vue-components": "^0.24.1",
"vite": "^4.3.7",
"vitest": "^0.31.0",
"vue-tsc": "^1.6.5"
}
}

View File

@ -6,31 +6,24 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import NavBar from '@/components/layout/NavBar.vue';
import NavFooter from '@/components/layout/NavFooter.vue';
import BodyLayout from '@/components/layout/BodyLayout.vue';
import { setTimezone } from './shared/formatters';
import { defineComponent, onMounted, watch } from 'vue';
import { onMounted, watch } from 'vue';
import { useSettingsStore } from './stores/settings';
export default defineComponent({
name: 'App',
components: { NavBar, BodyLayout, NavFooter },
setup() {
const settingsStore = useSettingsStore();
onMounted(() => {
setTimezone(settingsStore.timezone);
});
watch(
() => settingsStore.timezone,
(tz) => {
console.log('timezone changed', tz);
setTimezone(tz);
},
);
return {};
},
const settingsStore = useSettingsStore();
onMounted(() => {
setTimezone(settingsStore.timezone);
});
watch(
() => settingsStore.timezone,
(tz) => {
console.log('timezone changed', tz);
setTimezone(tz);
},
);
</script>
<style scoped>

View File

@ -11,18 +11,18 @@
switch
@change="changeEvent"
>
<OnlineIcon
<div
v-if="botStore.botStores[bot.botId].isBotLoggedIn"
:size="18"
class="ms-2 me-1 align-middle"
:class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'"
:title="botStore.botStores[bot.botId].isBotOnline ? 'Online' : 'Offline'"
></OnlineIcon>
<LoggedOutIcon
v-else
class="offline"
title="Login info expired, please login again."
></LoggedOutIcon>
>
<i-mdi-circle
class="ms-2 me-1 align-middle"
:class="botStore.botStores[bot.botId].isBotOnline ? 'online' : 'offline'"
/>
</div>
<div v-else title="Login info expired, please login again.">
<i-mdi-cancel class="offline" />
</div>
</b-form-checkbox>
<div v-if="!noButtons" class="float-end d-flex flex-align-center">
<b-button
@ -32,13 +32,13 @@
title="Edit bot"
@click="$emit('edit')"
>
<EditIcon :size="16" />
<i-mdi-pencil />
</b-button>
<b-button v-else class="ms-1" size="sm" title="Login again" @click="$emit('editLogin')">
<LoginIcon :size="16" />
<i-mdi-login />
</b-button>
<b-button class="ms-1" size="sm" title="Delete bot" @click="botRemoveModalVisible = true">
<DeleteIcon :size="16" title="Delete Bot" />
<i-mdi-delete />
</b-button>
</div>
</div>
@ -54,59 +54,34 @@
</div>
</template>
<script lang="ts">
import EditIcon from 'vue-material-design-icons/Pencil.vue';
import LoginIcon from 'vue-material-design-icons/Login.vue';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import OnlineIcon from 'vue-material-design-icons/Circle.vue';
import LoggedOutIcon from 'vue-material-design-icons/Cancel.vue';
import { BotDescriptor } from '@/types';
import { defineComponent, computed, ref } from 'vue';
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { BotDescriptor } from '@/types';
import { computed, ref } from 'vue';
export default defineComponent({
name: 'BotEntry',
components: {
DeleteIcon,
EditIcon,
LoginIcon,
OnlineIcon,
LoggedOutIcon,
const props = defineProps({
bot: { required: true, type: Object as () => BotDescriptor },
noButtons: { default: false, type: Boolean },
});
defineEmits(['edit', 'editLogin']);
const botStore = useBotStore();
const changeEvent = (v) => {
botStore.botStores[props.bot.botId].setAutoRefresh(v);
};
const botRemoveModalVisible = ref(false);
const confirmRemoveBot = () => {
botRemoveModalVisible.value = false;
botStore.removeBot(props.bot.botId);
console.log('removing bot.');
};
const autoRefreshLoc = computed({
get() {
return botStore.botStores[props.bot.botId].autoRefresh;
},
props: {
bot: { required: true, type: Object as () => BotDescriptor },
noButtons: { default: false, type: Boolean },
},
emits: ['edit', 'editLogin'],
setup(props) {
const botStore = useBotStore();
const changeEvent = (v) => {
botStore.botStores[props.bot.botId].setAutoRefresh(v);
};
const botRemoveModalVisible = ref(false);
const confirmRemoveBot = () => {
botRemoveModalVisible.value = false;
botStore.removeBot(props.bot.botId);
console.log('removing bot.');
};
const autoRefreshLoc = computed({
get() {
return botStore.botStores[props.bot.botId].autoRefresh;
},
set() {
// pass
},
});
return {
botStore,
changeEvent,
autoRefreshLoc,
confirmRemoveBot,
botRemoveModalVisible,
};
set() {
// pass
},
});
</script>

View File

@ -13,7 +13,7 @@
class="d-flex"
@click="botStore.selectBot(bot.botId)"
>
<ReorderIcon v-if="!small" class="handle me-2" />
<i-mdi-reorder-horizontal v-if="!small" class="handle me-2 fs-4" />
<bot-rename
v-if="editingBots.includes(bot.botId)"
:bot="bot"
@ -35,15 +35,14 @@
</template>
<script setup lang="ts">
import LoginModal from '@/views/LoginModal.vue';
import BotEntry from '@/components/BotEntry.vue';
import BotRename from '@/components/BotRename.vue';
import ReorderIcon from 'vue-material-design-icons/ReorderHorizontal.vue';
import LoginModal from '@/views/LoginModal.vue';
import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { AuthStorageWithBotId, BotDescriptor } from '@/types';
import { useSortable } from '@vueuse/integrations/useSortable';
import { computed, ref } from 'vue';
defineProps({
small: { default: false, type: Boolean },

View File

@ -23,6 +23,14 @@
:state="urlState === '' ? null : urlState"
@keydown.enter="handleOk"
></b-form-input>
<b-alert
v-if="urlDuplicate"
class="mt-2 p-1 alert-wrap"
:model-value="true"
variant="warning"
>
This URL is already in use by another bot.
</b-alert>
</b-form-group>
<b-form-group
:state="nameState"
@ -78,7 +86,7 @@
import { useUserService } from '@/shared/userService';
import { AuthPayload, AuthStorageWithBotId } from '@/types';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
@ -113,10 +121,16 @@ const emitLoginResult = (value: boolean) => {
emit('loginResult', value);
};
const urlDuplicate = computed<boolean>(() => {
const bots = Object.values(botStore.availableBots).find((bot) => bot.botUrl === auth.value.url);
return bots !== undefined;
});
const checkFormValidity = () => {
const valid = formRef.value?.checkValidity();
nameState.value = valid || auth.value.username !== '';
pwdState.value = valid || auth.value.password !== '';
urlState.value = valid || auth.value.url !== '';
return valid;
};

View File

@ -11,49 +11,34 @@
<div class="d-flex ms-2">
<b-button type="submit" size="sm" title="Save">
<CheckIcon :size="16" />
<i-mdi-check />
</b-button>
<b-button class="ms-1" size="sm" title="Cancel" @click="$emit('cancelled')">
<CloseIcon :size="16" />
<i-mdi-close />
</b-button>
</div>
</form>
</template>
<script lang="ts">
import CheckIcon from 'vue-material-design-icons/Check.vue';
import CloseIcon from 'vue-material-design-icons/Close.vue';
import { BotDescriptor } from '@/types';
import { defineComponent, ref } from 'vue';
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { BotDescriptor } from '@/types';
import { ref } from 'vue';
export default defineComponent({
name: 'BotRename',
components: {
CheckIcon,
CloseIcon,
},
props: {
bot: { type: Object as () => BotDescriptor, required: true },
},
emits: ['cancelled', 'saved'],
setup(props, { emit }) {
const botStore = useBotStore();
const newName = ref<string>(props.bot.botName);
const save = () => {
botStore.updateBot(props.bot.botId, {
botName: newName.value,
});
emit('saved');
};
return {
newName,
save,
};
},
const props = defineProps({
bot: { type: Object as () => BotDescriptor, required: true },
});
const emit = defineEmits(['cancelled', 'saved']);
const botStore = useBotStore();
const newName = ref<string>(props.bot.botName);
const save = () => {
botStore.updateBot(props.bot.botId, {
botName: newName.value,
});
emit('saved');
};
</script>

View File

@ -1,13 +1,12 @@
<template>
<b-link variant="outline-primary" class="nav-link" @click="toggleNight">
<ThemeLightDark :size="16" />
<i-mdi-brightness-6 />
</b-link>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import ThemeLightDark from 'vue-material-design-icons/Brightness6.vue';
import { useSettingsStore } from '@/stores/settings';
import { onMounted, ref } from 'vue';
const activeTheme = ref('');
const settingsStore = useSettingsStore();

View File

@ -1,5 +1,5 @@
<template>
<v-chart
<e-charts
v-if="currencies"
:option="balanceChartOptions"
:theme="settingsStore.chartTheme"
@ -7,7 +7,7 @@
/>
</template>
<script lang="ts">
<script setup lang="ts">
import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts';
@ -22,9 +22,9 @@ import {
TooltipComponent,
} from 'echarts/components';
import { BalanceRecords } from '@/types';
import { BalanceValues } from '@/types';
import { formatPriceCurrency } from '@/shared/formatters';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useSettingsStore } from '@/stores/settings';
use([
@ -37,67 +37,57 @@ use([
LabelLayout,
]);
export default defineComponent({
name: 'BalanceChart',
components: {
'v-chart': ECharts,
},
props: {
currencies: { required: true, type: Array as () => BalanceRecords[] },
showTitle: { required: false, type: Boolean },
},
setup(props) {
const settingsStore = useSettingsStore();
const props = defineProps({
currencies: { required: true, type: Array as () => BalanceValues[] },
showTitle: { required: false, type: Boolean },
});
const settingsStore = useSettingsStore();
const balanceChartOptions = computed((): EChartsOption => {
return {
title: {
text: 'Balance',
show: props.showTitle,
const balanceChartOptions = computed((): EChartsOption => {
return {
title: {
text: 'Balance',
show: props.showTitle,
},
center: ['50%', '50%'],
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['balance', 'currency', 'est_stake', 'free', 'used', 'stake'],
source: props.currencies,
},
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${formatPriceCurrency(params.value.balance, params.value.currency, 8)}<br />${
params.percent
}% (${formatPriceCurrency(params.value.est_stake, params.value.stake)})`;
},
},
// legend: {
// orient: 'vertical',
// right: 10,
// top: 20,
// bottom: 20,
// },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
encode: {
value: 'est_stake',
itemName: 'currency',
tooltip: ['balance', 'currency'],
},
center: ['50%', '50%'],
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['balance', 'currency', 'est_stake', 'free', 'used', 'stake'],
source: props.currencies,
label: {
formatter: '{b} - {d}%',
},
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${formatPriceCurrency(params.value.balance, params.value.currency, 8)}<br />${
params.percent
}% (${formatPriceCurrency(params.value.est_stake, params.value.stake)})`;
},
show: true,
},
// legend: {
// orient: 'vertical',
// right: 10,
// top: 20,
// bottom: 20,
// },
series: [
{
type: 'pie',
radius: ['40%', '70%'],
encode: {
value: 'est_stake',
itemName: 'currency',
tooltip: ['balance', 'currency'],
},
label: {
formatter: '{b} - {d}%',
},
tooltip: {
show: true,
},
},
],
};
});
return { balanceChartOptions, settingsStore };
},
},
],
};
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,9 @@
>
</v-select>
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">&#x21bb;</b-button>
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">
<i-mdi-refresh />
</b-button>
<small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals"
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
>
@ -35,18 +37,12 @@
>
<div class="ms-2">
<b-form-select
v-model="plotStore.plotConfigName"
:options="plotStore.availablePlotConfigNames"
size="sm"
@change="plotStore.plotConfigChanged"
>
</b-form-select>
<plot-config-select></plot-config-select>
</div>
<div class="ms-2 me-0 me-md-1">
<b-button size="sm" title="Plot configurator" @click="showConfigurator">
&#9881;
<i-mdi-cog width="12" height="12" />
</b-button>
</div>
</div>
@ -91,15 +87,16 @@
</template>
<script setup lang="ts">
import { Trade, PairHistory, LoadingStatus, ChartSliderPosition } from '@/types';
import CandleChart from '@/components/charts/CandleChart.vue';
import PlotConfigSelect from '@/components/charts/PlotConfigSelect.vue';
import PlotConfigurator from '@/components/charts/PlotConfigurator.vue';
import vSelect from 'vue-select';
import { useSettingsStore } from '@/stores/settings';
import { usePlotConfigStore } from '@/stores/plotConfig';
import { useSettingsStore } from '@/stores/settings';
import { ChartSliderPosition, LoadingStatus, PairHistory, Trade } from '@/types';
import vSelect from 'vue-select';
import { ref, computed, onMounted, watch } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { computed, onMounted, ref, watch } from 'vue';
const props = defineProps({
trades: { required: false, default: () => [], type: Array as () => Trade[] },
@ -111,6 +108,7 @@ const props = defineProps({
timerange: { required: false, default: '', type: String },
/** Only required if historicView is true */
strategy: { required: false, default: '', type: String },
freqaiModel: { required: false, default: undefined, type: String },
sliderPosition: {
required: false,
type: Object as () => ChartSliderPosition,
@ -176,6 +174,7 @@ const refresh = () => {
timeframe: props.timeframe,
timerange: props.timerange,
strategy: props.strategy,
freqaimodel: props.freqaiModel,
});
} else {
botStore.activeBot.getPairCandles({

View File

@ -1,8 +1,8 @@
<template>
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" />
<e-charts v-if="trades" ref="chart" autoresize manual-update :theme="settingsStore.chartTheme" />
</template>
<script lang="ts">
<script setup lang="ts">
import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts';
@ -17,10 +17,20 @@ import {
TooltipComponent,
} from 'echarts/components';
import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types';
import { defineComponent, computed, ComputedRef } from 'vue';
import {
ClosedTrade,
CumProfitData,
CumProfitDataPerDate,
CumProfitChartData,
Trade,
} from '@/types';
import { computed } from 'vue';
import { useSettingsStore } from '@/stores/settings';
import { dataZoomPartial } from '@/shared/charts/chartZoom';
import { ref } from 'vue';
import { onMounted } from 'vue';
import { watch } from 'vue';
import { watchThrottled } from '@vueuse/core';
use([
BarChart,
@ -38,158 +48,226 @@ use([
// Define Column labels here to avoid typos
const CHART_PROFIT = 'Profit';
export default defineComponent({
name: 'CumProfitChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean },
profitColumn: { default: 'profit_abs', type: String },
},
setup(props) {
const settingsStore = useSettingsStore();
// const botList = ref<string[]>([]);
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
const props = defineProps({
trades: { required: true, type: Array as () => ClosedTrade[] },
openTrades: { required: true, type: Array as () => Trade[] },
showTitle: { default: true, type: Boolean },
profitColumn: { default: 'profit_abs', type: String },
});
const settingsStore = useSettingsStore();
// const botList = ref<string[]>([]);
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
const cumulativeData: ComputedRef<{ date: number; profit: number }[]> = computed(() => {
const res: CumProfitData[] = [];
const resD: CumProfitDataPerDate = {};
const closedTrades = props.trades
.slice()
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
let profit = 0.0;
const chart = ref<typeof ECharts>();
for (let i = 0, len = closedTrades.length; i < len; i += 1) {
const trade = closedTrades[i];
const openProfit = computed<number>(() => {
return props.openTrades.reduce((a, v) => a + v[props.profitColumn], 0);
});
if (trade.close_timestamp && trade[props.profitColumn]) {
profit += trade[props.profitColumn];
if (!resD[trade.close_timestamp]) {
// New timestamp
resD[trade.close_timestamp] = { profit, [trade.botId]: profit };
} else {
// Add to existing profit
resD[trade.close_timestamp].profit += trade[props.profitColumn];
if (resD[trade.close_timestamp][trade.botId]) {
resD[trade.close_timestamp][trade.botId] += trade[props.profitColumn];
} else {
resD[trade.close_timestamp][trade.botId] = profit;
}
}
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
const cumulativeData = computed<CumProfitChartData[]>(() => {
const res: CumProfitData[] = [];
const resD: CumProfitDataPerDate = {};
const closedTrades = props.trades
.slice()
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
let profit = 0.0;
for (let i = 0, len = closedTrades.length; i < len; i += 1) {
const trade = closedTrades[i];
if (trade.close_timestamp && trade[props.profitColumn]) {
profit += trade[props.profitColumn];
if (!resD[trade.close_timestamp]) {
// New timestamp
resD[trade.close_timestamp] = { profit, [trade.botId]: profit };
} else {
// Add to existing profit
resD[trade.close_timestamp].profit += trade[props.profitColumn];
if (resD[trade.close_timestamp][trade.botId]) {
resD[trade.close_timestamp][trade.botId] += trade[props.profitColumn];
} else {
resD[trade.close_timestamp][trade.botId] = profit;
}
}
// console.log(resD);
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
}
}
return Object.entries(resD).map(([k, v]) => {
const obj = { date: parseInt(k, 10), profit: v.profit };
// TODO: The below could allow "lines" per bot"
// this.botList.forEach((botId) => {
// obj[botId] = v[botId];
// });
return obj;
});
});
const chartOptions = computed((): EChartsOption => {
const chartOptionsLoc: EChartsOption = {
title: {
text: 'Cumulative Profit',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'profit'],
source: cumulativeData.value,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [CHART_PROFIT],
right: '5%',
},
useUTC: false,
xAxis: {
type: 'time',
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 40,
},
],
grid: {
bottom: 80,
},
dataZoom: [
{
type: 'inside',
// xAxisIndex: [0],
start: 0,
end: 100,
},
{
// xAxisIndex: [0],
bottom: 10,
start: 0,
end: 100,
...dataZoomPartial,
},
],
series: [
{
type: 'line',
name: CHART_PROFIT,
animation: true,
step: 'end',
lineStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
itemStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
// symbol: 'none',
},
],
};
// TODO: maybe have profit lines per bot?
// this.botList.forEach((botId: string) => {
// console.log('bot', botId);
// chartOptionsLoc.series.push({
// type: 'line',
// name: botId,
// animation: true,
// step: 'end',
// lineStyle: {
// color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// itemStylesettingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// // symbol: 'none',
// });
const valueArray: CumProfitChartData[] = Object.entries(resD).map(
([k, v]: [string, CumProfitData]) => {
const obj = { date: parseInt(k, 10), profit: v.profit };
// TODO: The below could allow "lines" per bot"
// this.botList.forEach((botId) => {
// obj[botId] = v[botId];
// });
return chartOptionsLoc;
});
return obj;
},
);
return { settingsStore, cumulativeData, chartOptions };
},
if (props.openTrades.length > 0 && valueArray.length > 0) {
const lastPoint = valueArray[valueArray.length - 1];
if (lastPoint) {
const resultWitHOpen = (lastPoint.profit ?? 0) + openProfit.value;
valueArray.push({ date: lastPoint.date, currentProfit: lastPoint.profit });
// Add one day to date to ensure it's showing properly
const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
valueArray.push({ date: tomorrow, currentProfit: resultWitHOpen });
}
}
return valueArray;
});
function updateChart(initial = false) {
const chartOptionsLoc: EChartsOption = {
dataset: {
dimensions: ['date', 'profit', 'currentProfit'],
source: cumulativeData.value,
},
series: [
{
// Keep current-profit before profit, so the starting symbol is behind
type: 'line',
name: 'currentProfit',
animation: initial,
tooltip: {
show: false,
},
lineStyle: {
color: openProfit.value > 0 ? 'green' : 'red',
type: 'dotted',
},
itemStyle: {
color: openProfit.value > 0 ? 'green' : 'red',
},
encode: {
x: 'date',
y: 'currentProfit',
},
},
{
type: 'line',
name: CHART_PROFIT,
animation: initial,
step: 'end',
lineStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
itemStyle: {
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
},
encode: {
x: 'date',
y: 'profit',
},
// symbol: 'none',
},
],
};
// TODO: maybe have profit lines per bot?
// this.botList.forEach((botId: string) => {
// console.log('bot', botId);
// chartOptionsLoc.series.push({
// type: 'line',
// name: botId,
// animation: true,
// step: 'end',
// lineStyle: {
// color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// itemStylesettingsStore.chartTheme === 'dark' ? '#c2c2c2' : 'black',
// },
// // symbol: 'none',
// });
// });
chart.value?.setOption(chartOptionsLoc, {
replaceMerge: ['series', 'dataset'],
noMerge: !initial,
});
}
function initializeChart() {
chart.value?.setOption({}, { noMerge: true });
const chartOptionsLoc: EChartsOption = {
title: {
text: 'Cumulative Profit',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [CHART_PROFIT],
right: '5%',
},
useUTC: false,
xAxis: {
type: 'time',
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 40,
},
],
grid: {
bottom: 80,
},
dataZoom: [
{
type: 'inside',
// xAxisIndex: [0],
start: 0,
end: 100,
},
{
// xAxisIndex: [0],
bottom: 10,
start: 0,
end: 100,
...dataZoomPartial,
},
],
};
chart.value?.setOption(chartOptionsLoc, { noMerge: true });
updateChart(true);
}
onMounted(() => {
initializeChart();
});
watchThrottled(
() => props.openTrades,
() => {
updateChart();
},
{ throttle: 60 * 1000 },
);
watchThrottled(
() => props.trades,
() => {
updateChart();
},
{ throttle: 60 * 1000 },
);
</script>
<style scoped>

View File

@ -1,5 +1,5 @@
<template>
<v-chart
<e-charts
v-if="dailyStats.data"
:option="dailyChartOptions"
:theme="settingsStore.chartTheme"
@ -7,8 +7,8 @@
/>
</template>
<script lang="ts">
import { defineComponent, computed, ComputedRef } from 'vue';
<script setup lang="ts">
import { computed, ComputedRef } from 'vue';
import ECharts from 'vue-echarts';
// import { EChartsOption } from 'echarts';
@ -44,125 +44,113 @@ use([
const CHART_ABS_PROFIT = 'Absolute profit';
const CHART_TRADE_COUNT = 'Trade Count';
export default defineComponent({
components: {
'v-chart': ECharts,
const props = defineProps({
dailyStats: {
type: Object as () => DailyReturnValue,
required: true,
},
props: {
dailyStats: {
type: Object as () => DailyReturnValue,
required: true,
},
showTitle: {
type: Boolean,
default: true,
},
showTitle: {
type: Boolean,
default: true,
},
});
setup(props) {
const settingsStore = useSettingsStore();
const absoluteMin = computed(() =>
props.dailyStats.data.reduce(
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
props.dailyStats.data[0]?.abs_profit,
),
);
const absoluteMax = computed(() =>
props.dailyStats.data.reduce(
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
props.dailyStats.data[0]?.abs_profit,
),
);
const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
return {
title: {
text: 'Daily profit',
show: props.showTitle,
const settingsStore = useSettingsStore();
const absoluteMin = computed(() =>
props.dailyStats.data.reduce(
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
props.dailyStats.data[0]?.abs_profit,
),
);
const absoluteMax = computed(() =>
props.dailyStats.data.reduce(
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
props.dailyStats.data[0]?.abs_profit,
),
);
const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
return {
title: {
text: 'Daily profit',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'abs_profit', 'trade_count'],
source: props.dailyStats.data,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'abs_profit', 'trade_count'],
source: props.dailyStats.data,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
right: '5%',
},
xAxis: [
},
},
legend: {
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
right: '5%',
},
xAxis: [
{
type: 'category',
},
],
visualMap: [
{
dimension: 1,
seriesIndex: 0,
show: false,
pieces: [
{
type: 'category',
max: 0.0,
min: absoluteMin.value,
color: 'red',
},
{
min: 0.0,
max: absoluteMax.value,
color: 'green',
},
],
visualMap: [
{
dimension: 1,
seriesIndex: 0,
show: false,
pieces: [
{
max: 0.0,
min: absoluteMin.value,
color: 'red',
},
{
min: 0.0,
max: absoluteMax.value,
color: 'green',
},
],
},
],
yAxis: [
{
type: 'value',
name: CHART_ABS_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 40,
},
{
type: 'value',
name: CHART_TRADE_COUNT,
nameRotate: 90,
nameLocation: 'middle',
nameGap: 30,
},
],
series: [
{
type: 'line',
name: CHART_ABS_PROFIT,
// Color is induced by visualMap
},
{
type: 'bar',
name: CHART_TRADE_COUNT,
itemStyle: {
color: 'rgba(150,150,150,0.3)',
},
yAxisIndex: 1,
},
],
};
});
return {
dailyChartOptions,
settingsStore,
};
},
},
],
yAxis: [
{
type: 'value',
name: CHART_ABS_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 40,
},
{
type: 'value',
name: CHART_TRADE_COUNT,
nameRotate: 90,
nameLocation: 'middle',
nameGap: 30,
},
],
series: [
{
type: 'line',
name: CHART_ABS_PROFIT,
// Color is induced by visualMap
},
{
type: 'bar',
name: CHART_TRADE_COUNT,
itemStyle: {
color: 'rgba(150,150,150,0.3)',
},
yAxisIndex: 1,
},
],
};
});
</script>

View File

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

View File

@ -0,0 +1,38 @@
<template>
<edit-value
v-model="plotStore.plotConfigName"
:allow-edit="allowEdit"
:allow-add="allowEdit"
editable-name="plot configuration"
@rename="plotStore.renamePlotConfig"
@delete="plotStore.deletePlotConfig"
@new="plotStore.newPlotConfig"
>
<b-form-select
v-model="plotStore.plotConfigName"
:options="plotStore.availablePlotConfigNames"
size="sm"
@change="plotStore.plotConfigChanged"
>
</b-form-select>
</edit-value>
</template>
<script setup lang="ts">
import EditValue from '@/components/general/EditValue.vue';
import { usePlotConfigStore } from '@/stores/plotConfig';
defineProps({
allowEdit: {
type: Boolean,
default: false,
},
editableName: {
type: String,
default: 'plot configuration',
},
});
const plotStore = usePlotConfigStore();
</script>
<style scoped></style>

View File

@ -1,120 +1,147 @@
<template>
<div v-if="columns">
<b-form-group label="Plot config name" label-for="idPlotConfigName">
<b-form-input id="idPlotConfigName" v-model="plotConfigNameLoc" size="sm"> </b-form-input>
<plot-config-select allow-edit></plot-config-select>
</b-form-group>
<div class="col-mb-3">
<hr />
<b-form-group label="Target" label-for="FieldSel">
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="3">
</b-form-select>
<b-form-group label="Target Plot" label-for="FieldSel">
<edit-value
v-model="selSubPlot"
:allow-edit="!isMainPlot"
allow-add
editable-name="plot configuration"
align-vertical
@new="addSubplot"
@delete="deleteSubplot"
@rename="renameSubplot"
>
<b-form-select id="FieldSel" v-model="selSubPlot" :options="subplots" :select-size="5">
</b-form-select>
</edit-value>
</b-form-group>
</div>
<b-form-group label="Add new plot" label-for="newSubPlot">
<b-input-group size="sm">
<b-form-input id="newSubPlot" v-model="newSubplotName" class="addPlot"></b-form-input>
<b-input-group-append>
<b-button :disabled="!newSubplotName" @click="addSubplot">+</b-button>
<b-button v-if="selSubPlot && selSubPlot != 'main_plot'" @click="delSubplot">-</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<hr />
<div>
<b-form-group label="Used indicators" label-for="selectedIndicators">
<b-form-group label="Indicators in this plot" label-for="selectedIndicators">
<b-form-select
id="selectedIndicators"
v-model="selIndicatorName"
:disabled="addNewIndicator"
:options="usedColumns"
:select-size="4"
>
</b-form-select>
</b-form-group>
</div>
<div>
<b-button
variant="primary"
title="Add indicator to plot"
size="sm"
:disabled="addNewIndicator"
@click="addNewIndicator = !addNewIndicator"
>
Add new indicator
</b-button>
<div class="d-flex flex-row mt-1">
<b-button
variant="secondary"
title="Remove indicator to plot"
size="sm"
:disabled="!selIndicatorName"
class="ms-1"
class="col"
@click="removeIndicator"
>
Remove indicator
</b-button>
<b-button
variant="primary"
title="Add indicator to plot"
size="sm"
class="ms-1 col"
:disabled="addNewIndicator"
@click="
addNewIndicator = !addNewIndicator;
selIndicatorName = '';
"
>
Add new indicator
</b-button>
</div>
<PlotIndicator
v-if="selIndicatorName || addNewIndicator"
<PlotIndicatorSelect
v-if="addNewIndicator"
:columns="columns"
class="mt-1"
label="Select indicator to add"
@indicator-selected="addNewIndicatorSelected"
/>
<plot-indicator
v-if="selIndicatorName"
v-model="selIndicator"
class="mt-1"
:columns="columns"
:add-new="addNewIndicator"
/>
<hr />
<div>
<b-button class="ms-1" variant="secondary" size="sm" @click="loadPlotConfig">Load</b-button>
<div class="d-flex flex-row">
<b-button
class="ms-1 col"
variant="secondary"
size="sm"
:disabled="addNewIndicator"
title="Reset to last saved configuration"
@click="loadPlotConfig"
>Reset</b-button
>
<!--
Does Resetting a config to "nothing" make sense, or can this be done via "delete / create"?
<b-button
class="ms-1 col"
variant="secondary"
size="sm"
:disabled="addNewIndicator"
title="Start with empty configuration"
@click="clearConfig"
>Reset</b-button
> -->
<b-button
:disabled="
(botStore.activeBot.isWebserverMode && botStore.activeBot.botApiVersion < 2.23) ||
!botStore.activeBot.isBotOnline
!botStore.activeBot.isBotOnline ||
addNewIndicator
"
class="ms-1"
class="ms-1 col"
variant="secondary"
size="sm"
@click="loadPlotConfigFromStrategy"
>
From strategy
</b-button>
<b-button
class="ms-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@click="resetConfig"
>Reset</b-button
>
<b-button
id="showButton"
class="ms-1"
class="ms-1 col"
variant="secondary"
size="sm"
:disabled="addNewIndicator"
title="Show configuration for easy transfer to a strategy"
@click="showConfig = !showConfig"
>Show</b-button
>{{ showConfig ? 'Hide' : 'Show' }}</b-button
>
<b-button
v-if="showConfig"
class="ms-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@click="loadConfigFromString"
>Load from String</b-button
>
<b-button
class="ms-1"
class="ms-1 col"
variant="primary"
size="sm"
data-toggle="tooltip"
:disabled="addNewIndicator"
title="Save configuration"
@click="savePlotConfig"
>Save</b-button
>
</div>
<b-button
v-if="showConfig"
class="ms-1 mt-1"
variant="secondary"
size="sm"
title="Load configuration from text box below"
@click="loadConfigFromString"
>Load from String</b-button
>
<div v-if="showConfig" class="col-mb-5 ms-1 mt-2">
<b-form-textarea
id="TextArea"
@ -129,14 +156,18 @@
</template>
<script setup lang="ts">
import { PlotConfig, EMPTY_PLOTCONFIG, IndicatorConfig } from '@/types';
import { getCustomPlotConfig } from '@/shared/storage';
import EditValue from '@/components/general/EditValue.vue';
import PlotConfigSelect from '@/components/charts/PlotConfigSelect.vue';
import PlotIndicator from '@/components/charts/PlotIndicator.vue';
import { showAlert } from '@/stores/alerts';
import { IndicatorConfig, PlotConfig } from '@/types';
import PlotIndicatorSelect from './PlotIndicatorSelect.vue';
import { computed, ref, onMounted } from 'vue';
import { deepClone } from '@/shared/deepClone';
import { useBotStore } from '@/stores/ftbotwrapper';
import { usePlotConfigStore } from '@/stores/plotConfig';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import randomColor from '@/shared/randomColor';
defineProps({
columns: { required: true, type: Array as () => string[] },
@ -146,10 +177,7 @@ defineProps({
const plotStore = usePlotConfigStore();
const botStore = useBotStore();
const plotConfig = ref<PlotConfig>(EMPTY_PLOTCONFIG);
const plotConfigNameLoc = ref('default');
const newSubplotName = ref('');
const selIndicatorName = ref('');
const addNewIndicator = ref(false);
const showConfig = ref(false);
@ -163,43 +191,40 @@ const isMainPlot = computed(() => {
const currentPlotConfig = computed(() => {
if (isMainPlot.value) {
return plotConfig.value.main_plot;
return plotStore.editablePlotConfig.main_plot;
}
return plotConfig.value.subplots[selSubPlot.value];
return plotStore.editablePlotConfig.subplots[selSubPlot.value];
});
const subplots = computed((): string[] => {
// Subplot keys (for selection window)
return ['main_plot', ...Object.keys(plotConfig.value.subplots)];
return ['main_plot', ...Object.keys(plotStore.editablePlotConfig.subplots)];
});
const usedColumns = computed((): string[] => {
if (isMainPlot.value) {
return Object.keys(plotConfig.value.main_plot);
return Object.keys(plotStore.editablePlotConfig.main_plot);
}
if (selSubPlot.value in plotConfig.value.subplots) {
return Object.keys(plotConfig.value.subplots[selSubPlot.value]);
if (selSubPlot.value in plotStore.editablePlotConfig.subplots) {
return Object.keys(plotStore.editablePlotConfig.subplots[selSubPlot.value]);
}
return [];
});
function addIndicator(newIndicator: Record<string, IndicatorConfig>) {
console.log(plotConfig.value);
// const { plotConfig.value } = this;
const name = Object.keys(newIndicator)[0];
const indicator = newIndicator[name];
if (isMainPlot.value) {
console.log(`Adding ${name} to MainPlot`);
plotConfig.value.main_plot[name] = { ...indicator };
// console.log(`Adding ${name} to MainPlot`);
plotStore.editablePlotConfig.main_plot[name] = { ...indicator };
} else {
console.log(`Adding ${name} to ${selSubPlot.value}`);
plotConfig.value.subplots[selSubPlot.value][name] = { ...indicator };
// console.log(`Adding ${name} to ${selSubPlot.value}`);
plotStore.editablePlotConfig.subplots[selSubPlot.value][name] = { ...indicator };
}
plotConfig.value = { ...plotConfig.value };
plotStore.editablePlotConfig = { ...plotStore.editablePlotConfig };
// Reset random color
addNewIndicator.value = false;
plotStore.setPlotConfig(plotConfig.value);
}
const selIndicator = computed({
@ -215,7 +240,6 @@ const selIndicator = computed({
return {};
},
set(newValue: Record<string, IndicatorConfig>) {
// console.log('newValue', newValue);
const name = Object.keys(newValue)[0];
// this.currentPlotConfig[this.selIndicatorName] = { ...newValue[name] };
// this.emitPlotConfig();
@ -229,7 +253,7 @@ const selIndicator = computed({
const plotConfigJson = computed({
get() {
return JSON.stringify(plotConfig.value, null, 2);
return JSON.stringify(plotStore.editablePlotConfig, null, 2);
},
set(newValue: string) {
try {
@ -243,55 +267,53 @@ const plotConfigJson = computed({
});
function removeIndicator() {
console.log(plotConfig.value);
// const { plotConfig } = this;
if (isMainPlot.value) {
console.log(`Removing ${selIndicatorName.value} from MainPlot`);
delete plotConfig.value.main_plot[selIndicatorName.value];
delete plotStore.editablePlotConfig.main_plot[selIndicatorName.value];
} else {
console.log(`Removing ${selIndicatorName.value} from ${selSubPlot.value}`);
delete plotConfig.value.subplots[selSubPlot.value][selIndicatorName.value];
delete plotStore.editablePlotConfig.subplots[selSubPlot.value][selIndicatorName.value];
}
plotConfig.value = { ...plotConfig.value };
console.log(plotConfig.value);
plotStore.editablePlotConfig = { ...plotStore.editablePlotConfig };
selIndicatorName.value = '';
plotStore.setPlotConfig(plotConfig.value);
}
function addSubplot() {
plotConfig.value.subplots = {
...plotConfig.value.subplots,
[newSubplotName.value]: {},
function addSubplot(newSubplotName: string) {
plotStore.editablePlotConfig.subplots = {
...plotStore.editablePlotConfig.subplots,
[newSubplotName]: {},
};
selSubPlot.value = newSubplotName.value;
newSubplotName.value = '';
plotStore.setPlotConfig(plotConfig.value);
selSubPlot.value = newSubplotName;
}
function delSubplot() {
delete plotConfig.value.subplots[selSubPlot.value];
plotConfig.value.subplots = { ...plotConfig.value.subplots };
selSubPlot.value = '';
plotStore.setPlotConfig(plotConfig.value);
function deleteSubplot(subplotName: string) {
delete plotStore.editablePlotConfig.subplots[subplotName];
// plotStore.editablePlotConfig.subplots = { ...plotStore.editablePlotConfig.subplots };
selSubPlot.value = subplots.value[subplots.value.length - 1];
}
function renameSubplot(oldName: string, newName: string) {
plotStore.editablePlotConfig.subplots[newName] = plotStore.editablePlotConfig.subplots[oldName];
delete plotStore.editablePlotConfig.subplots[oldName];
selSubPlot.value = newName;
}
function loadPlotConfig() {
plotConfig.value = getCustomPlotConfig(plotConfigNameLoc.value);
console.log(plotConfig.value);
console.log('loading config');
plotStore.setPlotConfig(plotConfig.value);
// Reset from store
plotStore.editablePlotConfig = deepClone(plotStore.customPlotConfigs[plotStore.plotConfigName]);
}
function loadConfigFromString() {
// this.plotConfig = JSON.parse();
if (tempPlotConfig.value !== undefined && tempPlotConfigValid.value) {
plotConfig.value = tempPlotConfig.value;
plotStore.setPlotConfig(plotConfig.value);
plotStore.editablePlotConfig = tempPlotConfig.value;
}
}
function resetConfig() {
plotConfig.value = { ...EMPTY_PLOTCONFIG };
}
// function clearConfig() {
// // Use empty config
// plotStore.editablePlotConfig = { ...EMPTY_PLOTCONFIG };
// }
async function loadPlotConfigFromStrategy() {
if (botStore.activeBot.isWebserverMode && !botStore.activeBot.strategy.strategy) {
showAlert(`No strategy selected, can't load plot config.`);
@ -300,8 +322,7 @@ async function loadPlotConfigFromStrategy() {
try {
await botStore.activeBot.getStrategyPlotConfig();
if (botStore.activeBot.strategyPlotConfig) {
plotConfig.value = botStore.activeBot.strategyPlotConfig;
plotStore.setPlotConfig(plotConfig.value);
plotStore.editablePlotConfig = botStore.activeBot.strategyPlotConfig;
}
} catch (data) {
//
@ -310,14 +331,45 @@ async function loadPlotConfigFromStrategy() {
}
function savePlotConfig() {
plotStore.saveCustomPlotConfig({ [plotConfigNameLoc.value]: plotConfig.value });
plotStore.saveCustomPlotConfig(plotConfigNameLoc.value, plotStore.editablePlotConfig);
}
function addNewIndicatorSelected(indicator?: string) {
addNewIndicator.value = false;
if (indicator) {
addIndicator({
[indicator]: {
color: randomColor(),
},
});
selIndicatorName.value = indicator;
}
}
watch(selSubPlot, () => {
// Deselect Indicator when switching selected plot
selIndicatorName.value = '';
});
watch(
() => plotStore.plotConfigName,
() => {
selIndicatorName.value = '';
// selSubPlot.value = '';
},
);
onMounted(() => {
// console.log('Config Mounted', props);
plotConfig.value = plotStore.plotConfig;
// Deep clone and assign to editable
plotStore.editablePlotConfig = deepClone(plotStore.plotConfig);
plotStore.isEditing = true;
plotConfigNameLoc.value = plotStore.plotConfigName;
});
onUnmounted(() => {
// TODO: Unmounted is not called when closing in Chart view
plotStore.isEditing = false;
});
</script>
<style scoped>

View File

@ -1,133 +1,89 @@
<template>
<div>
<div v-if="addNew">
<b-form-group label="Add indicator" label-for="indicatorSelector">
<b-input-group size="sm">
<b-form-input v-model="indicatorFilter" placeholder="Filter indicators"></b-form-input>
<b-input-group-append>
<Reset
class="pointer align-self-center ms-1"
:size="18"
@click="indicatorFilter = ''"
></Reset>
</b-input-group-append>
</b-input-group>
<div class="d-flex flex-col flex-xl-row justify-content-between mt-1">
<b-form-group class="col flex-grow-1" label="Type" label-for="plotTypeSelector">
<b-form-select
id="indicatorSelector"
v-model="selAvailableIndicator"
:options="filteredIndicators"
:select-size="4"
id="plotTypeSelector"
v-model="graphType"
size="sm"
:options="availableGraphTypes"
>
</b-form-select>
</b-form-group>
<b-form-group label="Color" label-for="colsel" size="sm" class="ms-xl-1 col">
<b-input-group>
<b-input-group-prepend>
<b-form-input
v-model="selColor"
type="color"
size="sm"
class="p-0"
style="max-width: 29px"
></b-form-input>
</b-input-group-prepend>
<b-form-input id="colsel" v-model="selColor" size="sm" class="flex-grow-1">
</b-form-input>
<b-input-group-append>
<b-button variant="primary" size="sm" @click="newColor">
<i-mdi-dice-multiple />
</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
</div>
<b-form-group label="Type" label-for="plotTypeSelector">
<b-form-select
id="plotTypeSelector"
v-model="graphType"
size="sm"
:options="availableGraphTypes"
@change="emitIndicator()"
>
</b-form-select>
</b-form-group>
<hr />
<b-form-group label="Color" label-for="colsel" size="sm">
<b-input-group>
<b-input-group-prepend>
<div :style="{ 'background-color': selColor }" class="colorbox me-2"></div>
<!-- <b-form-input
id="colsel"
v-model="selColor"
size="sm"
class="colorbox"
type="color"
:style="{ 'background-color': selColor }"
>
</b-form-input> -->
</b-input-group-prepend>
<b-form-input id="colsel" v-model="selColor" size="sm"> </b-form-input>
<b-input-group-append>
<b-button variant="primary" size="sm" @click="newColor">&#x21bb;</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<div class="d-flex d-flex-columns">
<b-button
v-if="addNew"
class="flex-grow-1"
variant="primary"
title="Add "
size="sm"
@click="emitIndicator"
>
Save indicator
</b-button>
<b-button
v-if="addNew"
class="ms-1 flex-grow-1"
variant="secondary"
title="Add "
size="sm"
@click="clickCancel"
>
Cancel
</b-button>
</div>
<PlotIndicatorSelect
v-if="graphType === ChartType.line"
v-model="fillTo"
:columns="columns"
class="mt-1"
label="Select indicator to add"
/>
</div>
</template>
<script setup lang="ts">
import { ChartType, IndicatorConfig } from '@/types';
import randomColor from '@/shared/randomColor';
import Reset from 'vue-material-design-icons/CloseCircleOutline.vue';
import PlotIndicatorSelect from '@/components/charts/PlotIndicatorSelect.vue';
import { computed, ref, watch } from 'vue';
import { watchDebounced } from '@vueuse/core';
const props = defineProps({
modelValue: { required: true, type: Object as () => Record<string, IndicatorConfig> },
columns: { required: true, type: Array as () => string[] },
addNew: { required: true, type: Boolean },
});
const emit = defineEmits(['update:modelValue']);
const selColor = ref(randomColor());
const graphType = ref<ChartType>(ChartType.line);
const availableGraphTypes = ref(Object.keys(ChartType));
const indicatorFilter = ref('');
const selAvailableIndicator = ref('');
const cancelled = ref(false);
const fillTo = ref('');
const filteredIndicators = computed(() => {
return props.columns.filter((col) =>
col.toLowerCase().includes(indicatorFilter.value.toLowerCase()),
);
});
const newColor = () => {
function newColor() {
selColor.value = randomColor();
};
}
const combinedIndicator = computed(() => {
const combinedIndicator = computed<IndicatorConfig>(() => {
if (cancelled.value || !selAvailableIndicator.value) {
return {};
}
const val: IndicatorConfig = {
color: selColor.value,
type: graphType.value,
};
if (fillTo.value && graphType.value === ChartType.line) {
val.fill_to = fillTo.value;
}
return {
[selAvailableIndicator.value]: {
color: selColor.value,
type: graphType.value,
},
[selAvailableIndicator.value]: val,
};
});
const emitIndicator = () => {
emit('update:modelValue', combinedIndicator.value);
};
const clickCancel = () => {
cancelled.value = true;
emitIndicator();
};
function emitIndicator() {
emit('update:modelValue', combinedIndicator.value);
}
watch(
() => props.modelValue,
@ -135,29 +91,26 @@ watch(
[selAvailableIndicator.value] = Object.keys(props.modelValue);
cancelled.value = false;
if (selAvailableIndicator.value && props.modelValue) {
selColor.value = props.modelValue[selAvailableIndicator.value].color || randomColor();
graphType.value = props.modelValue[selAvailableIndicator.value].type || ChartType.line;
const xx = props.modelValue[selAvailableIndicator.value];
selColor.value = xx.color || randomColor();
graphType.value = xx.type || ChartType.line;
fillTo.value = xx.fill_to || '';
}
},
{
immediate: true,
},
);
watch(selColor, () => {
if (!props.addNew) {
watchDebounced(
[selColor, graphType, fillTo],
() => {
emitIndicator();
}
});
},
{
debounce: 200,
},
);
</script>
<style scoped>
.colorbox {
border-radius: 50%;
margin-top: auto;
margin-bottom: auto;
height: 25px;
width: 25px;
vertical-align: center;
}
.pointer {
cursor: pointer;
}
</style>
<style scoped></style>

View File

@ -0,0 +1,48 @@
<template>
<div class="d-flex flex-row">
<b-form-group class="flex-grow-1" :label="label" label-for="indicatorSelector">
<v-select
v-model="selAvailableIndicator"
:options="columns"
size="sm"
:clearable="false"
@option:selected="emitIndicator"
>
</v-select>
</b-form-group>
<b-button size="sm" title="Abort" class="ms-1 mt-auto" variant="secondary" @click="abort">
<i-mdi-close />
</b-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import vSelect from 'vue-select';
const props = defineProps({
modelValue: { required: false, default: '', type: String },
columns: { required: true, type: Array as () => string[] },
label: { required: true, type: String },
});
const emit = defineEmits(['update:modelValue', 'indicatorSelected']);
const selAvailableIndicator = ref(props.modelValue || '');
function emitIndicator() {
emit('indicatorSelected', selAvailableIndicator.value);
emit('update:modelValue', selAvailableIndicator.value);
}
function abort() {
selAvailableIndicator.value = '';
emitIndicator();
}
watch(
() => props.modelValue,
(newValue) => {
selAvailableIndicator.value = newValue;
},
);
</script>
<style scoped></style>

View File

@ -1,12 +1,13 @@
<template>
<div class="d-flex flex-column h-100 position-relative">
<div class="flex-grow-1 order-2">
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" />
<e-charts v-if="trades" :option="chartOptions" autoresize :theme="settingsStore.chartTheme" />
</div>
<b-form-group
class="w-25 order-1"
class="order-1"
:class="showTitle ? 'ms-5 ps-5' : 'position-absolute'"
label="Bins"
style="width: 33%; min-width: 12rem"
label-for="input-bins"
label-cols="6"
content-cols="6"
@ -16,14 +17,15 @@
id="input-bins"
v-model="settingsStore.profitDistributionBins"
size="sm"
class="mt-1"
:options="binOptions"
></b-form-select>
</b-form-group>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
<script setup lang="ts">
import { computed } from 'vue';
import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts';
@ -57,92 +59,82 @@ use([
// Define Column labels here to avoid typos
const CHART_PROFIT = 'Trade count';
export default defineComponent({
name: 'ProfitDistributionChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean },
},
setup(props) {
const settingsStore = useSettingsStore();
// registerTransform(ecStat.transform.histogram);
// console.log(profits);
// const data = [[]];
const binOptions = [10, 15, 20, 25, 50];
const data = computed(() => {
const profits = props.trades.map((trade) => trade.profit_ratio);
const props = defineProps({
trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean },
});
const settingsStore = useSettingsStore();
// registerTransform(ecStat.transform.histogram);
// console.log(profits);
// const data = [[]];
const binOptions = [10, 15, 20, 25, 50];
const data = computed(() => {
const profits = props.trades.map((trade) => trade.profit_ratio);
return binData(profits, settingsStore.profitDistributionBins);
});
return binData(profits, settingsStore.profitDistributionBins);
});
const chartOptions = computed((): EChartsOption => {
const chartOptionsLoc: EChartsOption = {
title: {
text: 'Profit distribution',
show: props.showTitle,
const chartOptions = computed((): EChartsOption => {
const chartOptionsLoc: EChartsOption = {
title: {
text: 'Profit distribution',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
source: data.value,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
source: data.value,
},
},
legend: {
data: [CHART_PROFIT],
right: '5%',
},
xAxis: {
type: 'category',
name: 'Profit %',
nameLocation: 'middle',
nameGap: 25,
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [CHART_PROFIT],
right: '5%',
},
xAxis: {
type: 'category',
name: 'Profit %',
nameLocation: 'middle',
nameGap: 25,
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 35,
position: 'left',
},
],
// grid: {
// bottom: 80,
// },
nameRotate: 90,
nameLocation: 'middle',
nameGap: 35,
position: 'left',
},
],
// grid: {
// bottom: 80,
// },
series: [
{
type: 'bar',
name: CHART_PROFIT,
animation: true,
encode: {
x: 'x0',
y: 'y0',
},
series: [
{
type: 'bar',
name: CHART_PROFIT,
animation: true,
encode: {
x: 'x0',
y: 'y0',
},
// symbol: 'none',
},
],
};
return chartOptionsLoc;
});
// console.log(chartOptions);
return { settingsStore, chartOptions, binOptions };
},
// symbol: 'none',
},
],
};
return chartOptionsLoc;
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<v-chart
<e-charts
v-if="trades.length > 0"
:option="chartOptions"
autoresize
@ -7,7 +7,7 @@
/>
</template>
<script lang="ts">
<script setup lang="ts">
import ECharts from 'vue-echarts';
import { EChartsOption } from 'echarts';
@ -26,7 +26,7 @@ import {
import { ClosedTrade } from '@/types';
import { useSettingsStore } from '@/stores/settings';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { timestampms } from '@/shared/formatters';
import { dataZoomPartial } from '@/shared/charts/chartZoom';
@ -49,143 +49,132 @@ use([
const CHART_PROFIT = 'Profit %';
const CHART_COLOR = '#9be0a8';
export default defineComponent({
name: 'TradesLogChart',
components: {
'v-chart': ECharts,
},
props: {
trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean },
},
setup(props) {
const settingsStore = useSettingsStore();
const chartData = computed(() => {
const res: (number | string)[][] = [];
const sortedTrades = props.trades
.slice(0)
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
for (let i = 0, len = sortedTrades.length; i < len; i += 1) {
const trade = sortedTrades[i];
const entry = [
i,
(trade.profit_ratio * 100).toFixed(2),
trade.pair,
trade.botName,
timestampms(trade.close_timestamp),
trade.is_short === undefined || !trade.is_short ? 'Long' : 'Short',
];
res.push(entry);
}
return res;
});
const props = defineProps({
trades: { required: true, type: Array as () => ClosedTrade[] },
showTitle: { default: true, type: Boolean },
});
const settingsStore = useSettingsStore();
const chartData = computed(() => {
const res: (number | string)[][] = [];
const sortedTrades = props.trades
.slice(0)
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
for (let i = 0, len = sortedTrades.length; i < len; i += 1) {
const trade = sortedTrades[i];
const entry = [
i,
(trade.profit_ratio * 100).toFixed(2),
trade.pair,
trade.botName,
timestampms(trade.close_timestamp),
trade.is_short === undefined || !trade.is_short ? 'Long' : 'Short',
];
res.push(entry);
}
return res;
});
const chartOptions = computed((): EChartsOption => {
// const { chartData } = this;
// Show a maximum of 50 trades by default - allowing to zoom out further.
const datazoomStart =
chartData.value.length > 0 ? (1 - 50 / chartData.value.length) * 100 : 100;
return {
title: {
text: 'Trades log',
show: props.showTitle,
const chartOptions = computed((): EChartsOption => {
// const { chartData } = this;
// Show a maximum of 50 trades by default - allowing to zoom out further.
const datazoomStart = chartData.value.length > 0 ? (1 - 50 / chartData.value.length) * 100 : 100;
return {
title: {
text: 'Trades log',
show: props.showTitle,
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'profit'],
source: chartData.value,
},
tooltip: {
trigger: 'axis',
formatter: (params) => {
const botName = params[0].data[3] ? ` | ${params[0].data[3]}` : '';
return `${params[0].data[2]} | ${params[0].data[5]} ${botName}<br>${params[0].data[4]}<br>Profit ${params[0].data[1]} %`;
},
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
backgroundColor: 'rgba(0, 0, 0, 0)',
dataset: {
dimensions: ['date', 'profit'],
source: chartData.value,
},
tooltip: {
trigger: 'axis',
formatter: (params) => {
const botName = params[0].data[3] ? ` | ${params[0].data[3]}` : '';
return `${params[0].data[2]} | ${params[0].data[5]} ${botName}<br>${params[0].data[4]}<br>Profit ${params[0].data[1]} %`;
},
axisPointer: {
type: 'line',
label: {
backgroundColor: '#6a7985',
},
},
},
xAxis: {
type: 'value',
},
},
xAxis: {
type: 'value',
show: false,
},
yAxis: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
yAxis: [
nameRotate: 90,
nameLocation: 'middle',
nameGap: 30,
},
],
grid: {
bottom: 80,
},
dataZoom: [
{
type: 'inside',
start: datazoomStart,
end: 100,
},
{
bottom: 10,
start: datazoomStart,
end: 100,
...dataZoomPartial,
},
],
visualMap: [
{
show: true,
seriesIndex: 0,
pieces: [
{
type: 'value',
name: CHART_PROFIT,
splitLine: {
show: false,
},
nameRotate: 90,
nameLocation: 'middle',
nameGap: 30,
max: 0.0,
color: '#f84960',
},
{
min: 0.0,
color: '#2ed191',
},
],
grid: {
bottom: 80,
},
],
series: [
{
type: 'bar',
name: CHART_PROFIT,
barGap: '0%',
barCategoryGap: '0%',
animation: false,
label: {
show: true,
position: 'top',
rotate: 90,
offset: [7.5, 7.5],
formatter: '{@[1]} %',
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : '#3c3c3c',
},
encode: {
x: 0,
y: 1,
},
dataZoom: [
{
type: 'inside',
start: datazoomStart,
end: 100,
},
{
bottom: 10,
start: datazoomStart,
end: 100,
...dataZoomPartial,
},
],
visualMap: [
{
show: true,
seriesIndex: 0,
pieces: [
{
max: 0.0,
color: '#f84960',
},
{
min: 0.0,
color: '#2ed191',
},
],
},
],
series: [
{
type: 'bar',
name: CHART_PROFIT,
barGap: '0%',
barCategoryGap: '0%',
animation: false,
label: {
show: true,
position: 'top',
rotate: 90,
offset: [7.5, 7.5],
formatter: '{@[1]} %',
color: settingsStore.chartTheme === 'dark' ? '#c2c2c2' : '#3c3c3c',
},
encode: {
x: 0,
y: 1,
},
itemStyle: {
color: CHART_COLOR,
},
},
],
};
});
return { settingsStore, chartData, chartOptions };
},
itemStyle: {
color: CHART_COLOR,
},
},
],
};
});
</script>

View File

@ -6,7 +6,7 @@
aria-label="Refresh"
@click="botStore.activeBot.getBacktestHistory"
>
&#x21bb;
<i-mdi-refresh />
</button>
<p>
Load Historic results from disk. You can click on multiple results to load all of them into
@ -28,24 +28,15 @@
</div>
</template>
<script>
import { defineComponent, onMounted } from 'vue';
<script setup lang="ts">
import { onMounted } from 'vue';
import { timestampms } from '@/shared/formatters';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
setup() {
const botStore = useBotStore();
const botStore = useBotStore();
onMounted(() => {
botStore.activeBot.getBacktestHistory();
});
return {
timestampms,
botStore,
};
},
onMounted(() => {
botStore.activeBot.getBacktestHistory();
});
</script>

View File

@ -37,6 +37,7 @@
showRightBar ? 'col-md-8' : 'col-md-10'
} candle-chart-container px-0 h-100 align-self-stretch`"
:slider-position="sliderPosition"
:freqai-model="freqaiModel"
>
</CandleChartContainer>
<TradeListNav
@ -65,6 +66,7 @@ import { ChartSliderPosition, Trade } from '@/types';
defineProps({
timeframe: { required: true, type: String },
strategy: { required: true, type: String },
freqaiModel: { required: false, default: undefined, type: String },
timerange: { required: true, type: String },
pairlist: { required: true, type: Array as () => string[] },
trades: { required: true, type: Array as () => Trade[] },

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { PeriodicBreakdown } from '@/types';
import { TableField, TableItem } from 'bootstrap-vue-next';
import { computed, ref } from 'vue';
defineProps({
periodicBreakdown: {
type: Object as () => PeriodicBreakdown,
required: true,
},
});
const periodicBreakdownSelections = [
{ value: 'day', text: 'Days' },
{ value: 'week', text: 'Weeks' },
{ value: 'month', text: 'Months' },
];
const periodicBreakdownPeriod = ref<string>('day');
const periodicBreakdownFields = computed<TableField[]>(() => {
return [
{ key: 'date', label: 'Date' },
{ key: 'wins', label: 'Wins' },
{ key: 'draws', label: 'Draws' },
{ key: 'loses', label: 'Losses' },
];
});
</script>
<template>
<b-form-radio-group
id="order-direction"
v-model="periodicBreakdownPeriod"
:options="periodicBreakdownSelections"
name="radios-btn-default"
size="sm"
buttons
style="min-width: 10em"
button-variant="outline-primary"
></b-form-radio-group>
<b-table
small
hover
stacked="sm"
:items="periodicBreakdown[periodicBreakdownPeriod] as unknown as TableItem[]"
:fields="periodicBreakdownFields"
>
</b-table>
</template>

View File

@ -16,32 +16,21 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { formatPercent } from '@/shared/formatters';
import { StrategyBacktestResult } from '@/types';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'BacktestResultSelect',
props: {
backtestHistory: {
required: true,
type: Object as () => Record<string, StrategyBacktestResult>,
},
selectedBacktestResultKey: { required: false, default: '', type: String },
},
emits: ['selectionChange'],
setup(_, { emit }) {
const setBacktestResult = (key) => {
emit('selectionChange', key);
};
return {
formatPercent,
setBacktestResult,
};
defineProps({
backtestHistory: {
required: true,
type: Object as () => Record<string, StrategyBacktestResult>,
},
selectedBacktestResultKey: { required: false, default: '', type: String },
});
const emit = defineEmits(['selectionChange']);
const setBacktestResult = (key) => {
emit('selectionChange', key);
};
</script>
<style scoped></style>

View File

@ -44,6 +44,15 @@
>
</b-table>
</b-card>
<b-card
v-if="backtestResult.periodic_breakdown"
header="Periodic breakdown"
class="row mt-2 w-100"
>
<BacktestResultPeriodBreakdown
:periodic-breakdown="backtestResult.periodic_breakdown"
></BacktestResultPeriodBreakdown>
</b-card>
<b-card header="Single trades" class="row mt-2 w-100">
<TradeList
@ -60,6 +69,7 @@
<script setup lang="ts">
import TradeList from '@/components/ftbot/TradeList.vue';
import { StrategyBacktestResult, Trade } from '@/types';
import BacktestResultPeriodBreakdown from './BacktestResultPeriodBreakdown.vue';
import { computed } from 'vue';
import {

View File

@ -4,20 +4,29 @@
<label class="me-auto h3">Balance</label>
<div class="float-end d-flex flex-row">
<b-button
v-if="canUseBotBalance"
size="sm"
title="Hide small balances"
:title="!showBotOnly ? 'Showing Account balance' : 'Showing Bot balance'"
@click="showBotOnly = !showBotOnly"
>
<i-mdi-robot v-if="showBotOnly" />
<i-mdi-bank v-else />
</b-button>
<b-button
size="sm"
:title="!hideSmallBalances ? 'Hide small balances' : 'Show all balances'"
@click="hideSmallBalances = !hideSmallBalances"
>
<HideIcon v-if="hideSmallBalances" :size="16" />
<ShowIcon v-else :size="16" />
<i-mdi-eye-off v-if="hideSmallBalances" />
<i-mdi-eye v-else />
</b-button>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance"
>&#x21bb;</b-button
>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getBalance">
<i-mdi-refresh />
</b-button>
</div>
</div>
<BalanceChart v-if="balanceCurrencies" :currencies="balanceCurrencies" />
<BalanceChart v-if="balanceCurrencies" :currencies="chartValues" />
<div>
<p v-if="botStore.activeBot.balance.note">
<strong>{{ botStore.activeBot.balance.note }}</strong>
@ -36,7 +45,11 @@
</td>
<!-- this is a computed prop that adds up all the expenses in the visible rows -->
<td>
<strong>{{ formatCurrency(botStore.activeBot.balance.total) }}</strong>
<strong>{{
showBotOnly && canUseBotBalance
? formatCurrency(botStore.activeBot.balance.total_bot)
: formatCurrency(botStore.activeBot.balance.total)
}}</strong>
</td>
</template>
</b-table>
@ -45,38 +58,64 @@
</template>
<script setup lang="ts">
import HideIcon from 'vue-material-design-icons/EyeOff.vue';
import ShowIcon from 'vue-material-design-icons/Eye.vue';
import BalanceChart from '@/components/charts/BalanceChart.vue';
import { formatPercent, formatPrice } from '@/shared/formatters';
import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { BalanceValues } from '@/types';
import { TableField } from 'bootstrap-vue-next';
import { computed, ref } from 'vue';
const botStore = useBotStore();
const hideSmallBalances = ref(true);
const showBotOnly = ref(true);
const smallBalance = computed((): number => {
return Number((0.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8));
const smallBalance = computed<number>(() => {
return Number((1.1 ** botStore.activeBot.stakeCurrencyDecimals).toFixed(8));
});
const canUseBotBalance = computed(() => {
return botStore.activeBot.botApiVersion >= 2.26;
});
const balanceCurrencies = computed(() => {
if (!hideSmallBalances.value) {
return botStore.activeBot.balance.currencies;
}
return botStore.activeBot.balance.currencies?.filter((v) => v.est_stake >= smallBalance.value);
return botStore.activeBot.balance.currencies?.filter(
(v) =>
(!hideSmallBalances.value || v.est_stake >= smallBalance.value) &&
(!canUseBotBalance.value || !showBotOnly.value || (v.is_bot_managed ?? true) === true),
);
});
const formatCurrency = (value) => {
return value ? formatPrice(value, 5) : '';
return value ? formatPrice(value, botStore.activeBot.stakeCurrencyDecimals) : '';
};
const tableFields = computed(() => {
const chartValues = computed<BalanceValues[]>(() => {
return balanceCurrencies.value?.map((v) => {
return {
balance:
showBotOnly.value && canUseBotBalance.value && v.bot_owned != undefined
? v.bot_owned
: v.balance,
currency: v.currency,
est_stake:
showBotOnly.value && canUseBotBalance.value ? v.est_stake_bot ?? v.est_stake : v.est_stake,
free: showBotOnly.value && canUseBotBalance.value ? v.bot_owned ?? v.free : v.free,
used: v.used,
stake: v.stake,
};
});
});
const tableFields = computed<TableField[]>(() => {
return [
{ key: 'currency', label: 'Currency' },
{ key: 'free', label: 'Available', formatter: formatCurrency },
{
key: 'est_stake',
key: showBotOnly.value && canUseBotBalance.value ? 'bot_owned' : 'free',
label: 'Available',
formatter: formatCurrency,
},
{
key: showBotOnly.value && canUseBotBalance.value ? 'est_stake_bot' : 'est_stake',
label: `in ${botStore.activeBot.balance.stake}`,
formatter: formatCurrency,
},

View File

@ -52,86 +52,73 @@
</b-table>
</template>
<script lang="ts">
<script setup lang="ts">
import ProfitPill from '@/components/general/ProfitPill.vue';
import { formatPrice } from '@/shared/formatters';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { ProfitInterface, ComparisonTableItems } from '@/types';
import { TableField, TableItem } from 'bootstrap-vue-next';
export default defineComponent({
name: 'BotComparisonList',
components: { ProfitPill },
setup() {
const botStore = useBotStore();
const botStore = useBotStore();
const tableFields: TableField[] = [
{ key: 'botName', label: 'Bot' },
{ key: 'trades', label: 'Trades' },
{ key: 'profitOpen', label: 'Open Profit' },
{ key: 'profitClosed', label: 'Closed Profit' },
{ key: 'balance', label: 'Balance' },
{ key: 'winVsLoss', label: 'W/L' },
];
const tableFields: TableField[] = [
{ key: 'botName', label: 'Bot' },
{ key: 'trades', label: 'Trades' },
{ key: 'profitOpen', label: 'Open Profit' },
{ key: 'profitClosed', label: 'Closed Profit' },
{ key: 'balance', label: 'Balance' },
{ key: 'winVsLoss', label: 'W/L' },
];
const tableItems = computed<TableItem[]>(() => {
const val: ComparisonTableItems[] = [];
const summary: ComparisonTableItems = {
botId: undefined,
botName: 'Summary',
profitClosed: 0,
profitClosedRatio: undefined,
profitOpen: 0,
profitOpenRatio: undefined,
stakeCurrency: 'USDT',
wins: 0,
losses: 0,
};
const tableItems = computed<TableItem[]>(() => {
const val: ComparisonTableItems[] = [];
const summary: ComparisonTableItems = {
botId: undefined,
botName: 'Summary',
profitClosed: 0,
profitClosedRatio: undefined,
profitOpen: 0,
profitOpenRatio: undefined,
stakeCurrency: 'USDT',
wins: 0,
losses: 0,
};
Object.entries(botStore.allProfit).forEach(([k, v]: [k: string, v: ProfitInterface]) => {
const allStakes = botStore.allOpenTrades[k].reduce((a, b) => a + b.stake_amount, 0);
const profitOpenRatio =
botStore.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) /
allStakes;
const profitOpen = botStore.allOpenTrades[k].reduce((a, b) => a + (b.profit_abs ?? 0), 0);
Object.entries(botStore.allProfit).forEach(([k, v]: [k: string, v: ProfitInterface]) => {
const allStakes = botStore.allOpenTrades[k].reduce((a, b) => a + b.stake_amount, 0);
const profitOpenRatio =
botStore.allOpenTrades[k].reduce((a, b) => a + b.profit_ratio * b.stake_amount, 0) /
allStakes;
const profitOpen = botStore.allOpenTrades[k].reduce((a, b) => a + (b.profit_abs ?? 0), 0);
// TODO: handle one inactive bot ...
val.push({
botId: k,
botName: botStore.availableBots[k].botName,
trades: `${botStore.allOpenTradeCount[k]} / ${
botStore.allBotState[k]?.max_open_trades || 'N/A'
}`,
profitClosed: v.profit_closed_coin,
profitClosedRatio: v.profit_closed_ratio || 0,
stakeCurrency: botStore.allBotState[k]?.stake_currency || '',
profitOpenRatio,
profitOpen,
wins: v.winning_trades,
losses: v.losing_trades,
balance: botStore.allBalance[k]?.total,
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
});
if (v.profit_closed_coin !== undefined) {
summary.profitClosed += v.profit_closed_coin;
summary.profitOpen += v.profit_all_coin;
summary.wins += v.winning_trades;
summary.losses += v.losing_trades;
// summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
}
});
val.push(summary);
return val as unknown as TableItem[];
// TODO: handle one inactive bot ...
val.push({
botId: k,
botName: botStore.availableBots[k].botName,
trades: `${botStore.allOpenTradeCount[k]} / ${
botStore.allBotState[k]?.max_open_trades || 'N/A'
}`,
profitClosed: v.profit_closed_coin,
profitClosedRatio: v.profit_closed_ratio || 0,
stakeCurrency: botStore.allBotState[k]?.stake_currency || '',
profitOpenRatio,
profitOpen,
wins: v.winning_trades,
losses: v.losing_trades,
balance: botStore.allBalance[k]?.total_bot ?? botStore.allBalance[k]?.total,
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
});
return {
formatPrice,
tableFields,
tableItems,
botStore,
};
},
if (v.profit_closed_coin !== undefined) {
summary.profitClosed += v.profit_closed_coin;
summary.profitOpen += v.profit_all_coin;
summary.wins += v.winning_trades;
summary.losses += v.losing_trades;
// summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
}
});
val.push(summary);
return val as unknown as TableItem[];
});
</script>

View File

@ -7,7 +7,7 @@ forceexit
title="Start Trading"
@click="botStore.activeBot.startBot()"
>
<PlayIcon />
<i-mdi-play height="24" width="24" />
</button>
<button
class="btn btn-secondary btn-sm ms-1"
@ -15,7 +15,7 @@ forceexit
title="Stop Trading - Also stops handling open trades."
@click="handleStopBot()"
>
<StopIcon />
<i-mdi-stop height="24" width="24" />
</button>
<button
class="btn btn-secondary btn-sm ms-1"
@ -23,7 +23,7 @@ forceexit
title="StopBuy - Stops buying, but still handles open trades"
@click="handleStopBuy()"
>
<PauseIcon />
<i-mdi-pause height="24" width="24" />
</button>
<button
class="btn btn-secondary btn-sm ms-1"
@ -31,7 +31,7 @@ forceexit
title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly."
@click="handleReloadConfig()"
>
<ReloadIcon />
<i-mdi-reload height="24" width="24" />
</button>
<button
class="btn btn-secondary btn-sm ms-1"
@ -39,20 +39,16 @@ forceexit
title="Force exit all"
@click="handleForceExit()"
>
<ForceExitIcon />
<i-mdi-close-box-multiple height="24" width="24" />
</button>
<button
v-if="
botStore.activeBot.botState &&
(botStore.activeBot.botState.force_entry_enable ||
botStore.activeBot.botState.forcebuy_enabled)
"
v-if="botStore.activeBot.botState && botStore.activeBot.botState.force_entry_enable"
class="btn btn-secondary btn-sm ms-1"
:disabled="!botStore.activeBot.isTrading || !isRunning"
title="Force enter - Immediately enter a trade at an optional price. Exits are then handled according to strategy rules."
@click="forceEnter = true"
>
<ForceEntryIcon />
<i-mdi-plus-box-multiple-outline style="font-size: 20px" />
</button>
<button
v-if="botStore.activeBot.isWebserverMode && false"
@ -61,105 +57,75 @@ forceexit
title="Start Trading mode"
@click="botStore.activeBot.startTrade()"
>
<PlayIcon />
<i-mdi-play class="fs-4" />
</button>
<ForceEntryForm v-model="forceEnter" :pair="botStore.activeBot.selectedPair" />
<MessageBox ref="msgBox" />
</div>
</template>
<script lang="ts">
import { ForceSellPayload } from '@/types';
import PlayIcon from 'vue-material-design-icons/Play.vue';
import StopIcon from 'vue-material-design-icons/Stop.vue';
import PauseIcon from 'vue-material-design-icons/Pause.vue';
import ReloadIcon from 'vue-material-design-icons/Reload.vue';
import ForceExitIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
import ForceEntryIcon from 'vue-material-design-icons/PlusBoxMultipleOutline.vue';
import ForceEntryForm from './ForceEntryForm.vue';
<script setup lang="ts">
import MessageBox, { MsgBoxObject } from '@/components/general/MessageBox.vue';
import { defineComponent, computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { ForceSellPayload } from '@/types';
import { computed, ref } from 'vue';
export default defineComponent({
name: 'BotControls',
components: {
ForceEntryForm,
PlayIcon,
StopIcon,
PauseIcon,
ReloadIcon,
ForceExitIcon,
ForceEntryIcon,
MessageBox,
},
setup() {
const botStore = useBotStore();
const forceEnter = ref<boolean>(false);
const msgBox = ref<typeof MessageBox>();
import ForceEntryForm from './ForceEntryForm.vue';
const isRunning = computed((): boolean => {
return botStore.activeBot.botState?.state === 'running';
});
const botStore = useBotStore();
const forceEnter = ref<boolean>(false);
const msgBox = ref<typeof MessageBox>();
const handleStopBot = () => {
const msg: MsgBoxObject = {
title: 'Stop Bot',
message: 'Stop the bot loop from running?',
accept: () => {
botStore.activeBot.stopBot();
},
};
msgBox.value?.show(msg);
};
const handleStopBuy = () => {
const msg: MsgBoxObject = {
title: 'Stop Buying',
message: 'Freqtrade will continue to handle open trades.',
accept: () => {
botStore.activeBot.stopBuy();
},
};
msgBox.value?.show(msg);
};
const handleReloadConfig = () => {
const msg: MsgBoxObject = {
title: 'Reload',
message: 'Reload configuration (including strategy)?',
accept: () => {
console.log('reload...');
botStore.activeBot.reloadConfig();
},
};
msgBox.value?.show(msg);
};
const handleForceExit = () => {
const msg: MsgBoxObject = {
title: 'ForceExit all',
message: 'Really forceexit ALL trades?',
accept: () => {
const payload: ForceSellPayload = {
tradeid: 'all',
// TODO: support ordertype (?)
};
botStore.activeBot.forceexit(payload);
},
};
msgBox.value?.show(msg);
};
return {
handleStopBot,
handleStopBuy,
handleReloadConfig,
handleForceExit,
forceEnter,
botStore,
isRunning,
msgBox,
};
},
const isRunning = computed((): boolean => {
return botStore.activeBot.botState?.state === 'running';
});
const handleStopBot = () => {
const msg: MsgBoxObject = {
title: 'Stop Bot',
message: 'Stop the bot loop from running?',
accept: () => {
botStore.activeBot.stopBot();
},
};
msgBox.value?.show(msg);
};
const handleStopBuy = () => {
const msg: MsgBoxObject = {
title: 'Stop Buying',
message: 'Freqtrade will continue to handle open trades.',
accept: () => {
botStore.activeBot.stopBuy();
},
};
msgBox.value?.show(msg);
};
const handleReloadConfig = () => {
const msg: MsgBoxObject = {
title: 'Reload',
message: 'Reload configuration (including strategy)?',
accept: () => {
console.log('reload...');
botStore.activeBot.reloadConfig();
},
};
msgBox.value?.show(msg);
};
const handleForceExit = () => {
const msg: MsgBoxObject = {
title: 'ForceExit all',
message: 'Really forceexit ALL trades?',
accept: () => {
const payload: ForceSellPayload = {
tradeid: 'all',
// TODO: support ordertype (?)
};
botStore.activeBot.forceexit(payload);
},
};
msgBox.value?.show(msg);
};
</script>

View File

@ -11,32 +11,23 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { formatPrice } from '@/shared/formatters';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { TableField } from 'bootstrap-vue-next';
export default defineComponent({
name: 'BotPerformance',
setup() {
const botStore = useBotStore();
const tableFields = computed<TableField[]>(() => {
return [
{ key: 'pair', label: 'Pair' },
{ key: 'profit', label: 'Profit %' },
{
key: 'profit_abs',
label: `Profit ${botStore.activeBot.botState?.stake_currency}`,
formatter: (v: unknown) => formatPrice(v as number, 5),
},
{ key: 'count', label: 'Count' },
];
});
return {
tableFields,
botStore,
};
},
const botStore = useBotStore();
const tableFields = computed<TableField[]>(() => {
return [
{ key: 'pair', label: 'Pair' },
{ key: 'profit', label: 'Profit %' },
{
key: 'profit_abs',
label: `Profit ${botStore.activeBot.botState?.stake_currency}`,
formatter: (v: unknown) => formatPrice(v as number, 5),
},
{ key: 'count', label: 'Count' },
];
});
</script>

View File

@ -27,13 +27,7 @@
<p>
Currently <strong>{{ botStore.activeBot.botState.state }}</strong
>,
<strong
>force entry:
{{
botStore.activeBot.botState.force_entry_enable ||
botStore.activeBot.botState.forcebuy_enabled
}}</strong
>
<strong>force entry: {{ botStore.activeBot.botState.force_entry_enable }}</strong>
</p>
<p>
<strong>{{ botStore.activeBot.botState.dry_run ? 'Dry-Run' : 'Live' }}</strong>
@ -85,23 +79,11 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'BotStatus',
components: { DateTimeTZ },
setup() {
const botStore = useBotStore();
return {
formatPercent,
formatPriceCurrency,
botStore,
};
},
});
const botStore = useBotStore();
</script>

View File

@ -33,70 +33,36 @@
</div>
</template>
<script lang="ts">
import { formatPrice } from '@/shared/formatters';
<script setup lang="ts">
import { Trade } from '@/types';
import CustomTradeListEntry from '@/components/ftbot/CustomTradeListEntry.vue';
import { defineComponent, computed, ref } from 'vue';
import { computed, ref } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'CustomTradeList',
components: {
CustomTradeListEntry,
},
props: {
trades: { required: true, type: Array as () => Trade[] },
title: { default: 'Trades', type: String },
stakeCurrency: { required: false, default: '', type: String },
activeTrades: { default: false, type: Boolean },
showFilter: { default: false, type: Boolean },
multiBotView: { default: false, type: Boolean },
emptyText: { default: 'No Trades to show.', type: String },
stakeCurrencyDecimals: { default: 3, type: Number },
},
setup(props) {
const botStore = useBotStore();
const currentPage = ref(1);
const filterText = ref('');
const perPage = props.activeTrades ? 200 : 25;
const rows = computed(() => props.trades.length);
const filteredTrades = computed(() => {
return props.trades.slice((currentPage.value - 1) * perPage, currentPage.value * perPage);
});
const formatPriceWithDecimals = (price) => {
return formatPrice(price, props.stakeCurrencyDecimals);
};
const handleContextMenuEvent = (item, index, event) => {
// stop browser context menu from appearing
if (!props.activeTrades) {
return;
}
event.preventDefault();
// log the selected item to the console
console.log(item);
};
const tradeClick = (trade) => {
botStore.activeBot.setDetailTrade(trade);
};
return {
currentPage,
filterText,
perPage,
filteredTrades,
formatPriceWithDecimals,
handleContextMenuEvent,
tradeClick,
botStore,
rows,
};
},
const props = defineProps({
trades: { required: true, type: Array as () => Trade[] },
title: { default: 'Trades', type: String },
stakeCurrency: { required: false, default: '', type: String },
activeTrades: { default: false, type: Boolean },
showFilter: { default: false, type: Boolean },
multiBotView: { default: false, type: Boolean },
emptyText: { default: 'No Trades to show.', type: String },
stakeCurrencyDecimals: { default: 3, type: Number },
});
const botStore = useBotStore();
const currentPage = ref(1);
const filterText = ref('');
const perPage = props.activeTrades ? 200 : 25;
const rows = computed(() => props.trades.length);
const filteredTrades = computed(() => {
return props.trades.slice((currentPage.value - 1) * perPage, currentPage.value * perPage);
});
const tradeClick = (trade) => {
botStore.activeBot.setDetailTrade(trade);
};
</script>
<style lang="scss" scoped>

View File

@ -15,39 +15,23 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { formatPercent, formatPrice } from '@/shared/formatters';
<script setup lang="ts">
import { Trade } from '@/types';
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
import TradeProfit from './TradeProfit.vue';
export default defineComponent({
components: {
DateTimeTZ,
TradeProfit,
defineProps({
trade: {
type: Object as () => Trade,
required: true,
},
props: {
trade: {
type: Object as () => Trade,
required: true,
},
stakeCurrencyDecimals: {
type: Number,
required: true,
},
showDetails: {
type: Boolean,
default: false,
},
stakeCurrencyDecimals: {
type: Number,
required: true,
},
setup() {
return {
formatPrice,
formatPercent,
};
showDetails: {
type: Boolean,
default: false,
},
});
</script>

View File

@ -2,7 +2,9 @@
<div>
<div class="mb-2">
<label class="me-auto h3">Daily Stats</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getDaily">&#x21bb;</b-button>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getDaily">
<i-mdi-refresh />
</b-button>
</div>
<div>
<DailyChart
@ -17,51 +19,38 @@
</div>
</template>
<script lang="ts">
import { defineComponent, computed, onMounted } from 'vue';
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import DailyChart from '@/components/charts/DailyChart.vue';
import { formatPercent } from '@/shared/formatters';
import { useBotStore } from '@/stores/ftbotwrapper';
import { TableField } from 'bootstrap-vue-next';
export default defineComponent({
name: 'DailyStats',
components: {
DailyChart,
},
setup() {
const botStore = useBotStore();
const dailyFields = computed<TableField[]>(() => {
const res: TableField[] = [
{ key: 'date', label: 'Day' },
{
key: 'abs_profit',
label: 'Profit',
// formatter: (value: unknown) => formatPrice(value as number),
},
{
key: 'fiat_value',
label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`,
// formatter: (value: unknown) => formatPrice(value as number, 2),
},
{ key: 'trade_count', label: 'Trades' },
];
if (botStore.activeBot.botApiVersion >= 2.16)
res.push({
key: 'rel_profit',
label: 'Profit%',
formatter: (value: unknown) => formatPercent(value as number, 2),
});
return res;
const botStore = useBotStore();
const dailyFields = computed<TableField[]>(() => {
const res: TableField[] = [
{ key: 'date', label: 'Day' },
{
key: 'abs_profit',
label: 'Profit',
// formatter: (value: unknown) => formatPrice(value as number),
},
{
key: 'fiat_value',
label: `In ${botStore.activeBot.dailyStats.fiat_display_currency}`,
// formatter: (value: unknown) => formatPrice(value as number, 2),
},
{ key: 'trade_count', label: 'Trades' },
];
if (botStore.activeBot.botApiVersion >= 2.16)
res.push({
key: 'rel_profit',
label: 'Profit%',
formatter: (value: unknown) => formatPercent(value as number, 2),
});
onMounted(() => {
botStore.activeBot.getDaily();
});
return {
botStore,
dailyFields,
};
},
return res;
});
onMounted(() => {
botStore.activeBot.getDaily();
});
</script>

View File

@ -33,7 +33,7 @@
class="me-1"
:class="botStore.activeBot.botApiVersion >= 1.12 ? 'col-6' : ''"
size="sm"
>+
><i-mdi-plus-box-outline />
</b-button>
<b-button
v-if="botStore.activeBot.botApiVersion >= 1.12"
@ -43,7 +43,7 @@
:disabled="blacklistSelect.length === 0"
@click="deletePairs"
>
<DeleteIcon :size="16" title="Delete Bot" />
<i-mdi-delete />
</b-button>
</div>
<b-popover
@ -81,7 +81,7 @@
class="pair black"
:active="blacklistSelect.indexOf(key) > -1"
@click="blacklistSelectClick(key)"
><span class="check">&#x2714;</span>{{ pair }}</b-list-group-item
><span class="check"><i-mdi-check-circle /></span>{{ pair }}</b-list-group-item
>
</b-list-group>
</div>
@ -91,89 +91,73 @@
</div>
</template>
<script lang="ts">
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import { defineComponent, ref, onMounted } from 'vue';
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { onMounted, ref } from 'vue';
export default defineComponent({
name: 'FTBotAPIPairList',
components: { DeleteIcon },
setup() {
const newblacklistpair = ref('');
const blackListShow = ref(false);
const blacklistSelect = ref<number[]>([]);
const botStore = useBotStore();
const newblacklistpair = ref('');
const blackListShow = ref(false);
const blacklistSelect = ref<number[]>([]);
const botStore = useBotStore();
const initBlacklist = () => {
if (botStore.activeBot.whitelist.length === 0) {
botStore.activeBot.getWhitelist();
}
if (botStore.activeBot.blacklist.length === 0) {
botStore.activeBot.getBlacklist();
}
};
const initBlacklist = () => {
if (botStore.activeBot.whitelist.length === 0) {
botStore.activeBot.getWhitelist();
}
if (botStore.activeBot.blacklist.length === 0) {
botStore.activeBot.getBlacklist();
}
};
const addBlacklistPair = () => {
if (newblacklistpair.value) {
blackListShow.value = false;
const addBlacklistPair = () => {
if (newblacklistpair.value) {
blackListShow.value = false;
botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] });
newblacklistpair.value = '';
}
};
botStore.activeBot.addBlacklist({ blacklist: [newblacklistpair.value] });
newblacklistpair.value = '';
}
};
const blacklistSelectClick = (key) => {
console.log(key);
const index = blacklistSelect.value.indexOf(key);
if (index > -1) {
blacklistSelect.value.splice(index, 1);
} else {
blacklistSelect.value.push(key);
}
};
const blacklistSelectClick = (key) => {
console.log(key);
const index = blacklistSelect.value.indexOf(key);
if (index > -1) {
blacklistSelect.value.splice(index, 1);
} else {
blacklistSelect.value.push(key);
}
};
const deletePairs = () => {
if (blacklistSelect.value.length === 0) {
console.log('nothing to delete');
return;
}
// const pairlist = blacklistSelect.value;
const pairlist = botStore.activeBot.blacklist.filter(
(value, index) => blacklistSelect.value.indexOf(index) > -1,
);
console.log('Deleting pairs: ', pairlist);
botStore.activeBot.deleteBlacklist(pairlist);
blacklistSelect.value = [];
};
onMounted(() => {
initBlacklist();
});
return {
addBlacklistPair,
deletePairs,
initBlacklist,
blacklistSelectClick,
botStore,
newblacklistpair,
blackListShow,
blacklistSelect,
};
},
const deletePairs = () => {
if (blacklistSelect.value.length === 0) {
console.log('nothing to delete');
return;
}
// const pairlist = blacklistSelect.value;
const pairlist = botStore.activeBot.blacklist.filter(
(value, index) => blacklistSelect.value.indexOf(index) > -1,
);
console.log('Deleting pairs: ', pairlist);
botStore.activeBot.deleteBlacklist(pairlist);
blacklistSelect.value = [];
};
onMounted(() => {
initBlacklist();
});
</script>
<style scoped lang="scss">
.check {
// Hidden checkbox on blacklist selection
background: #41b883;
// background: white;
color: #41b883;
opacity: 0;
border-radius: 50%;
// border-radius: 50%;
z-index: 5;
width: 1.3em;
height: 1.3em;
top: -0.2em;
left: -0.2em;
top: -0.3em;
left: -0.3em;
position: absolute;
transition: opacity 0.2s;
}

View File

@ -56,7 +56,6 @@
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*Stake-amount in ${botStore.activeBot.stakeCurrency} [optional]`"
label-for="stake-input"
invalid-feedback="Stake-amount must be empty or a positive number"
@ -86,7 +85,6 @@
></b-form-input>
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.1"
label="OrderType"
label-for="ordertype-input"
invalid-feedback="OrderType"
@ -202,14 +200,12 @@ const resetForm = () => {
selectedPair.value = props.pair;
price.value = undefined;
stakeAmount.value = undefined;
if (botStore.activeBot.botApiVersion > 1.1) {
ordertype.value =
botStore.activeBot.botState?.order_types?.forcebuy ||
botStore.activeBot.botState?.order_types?.force_entry ||
botStore.activeBot.botState?.order_types?.buy ||
botStore.activeBot.botState?.order_types?.entry ||
'limit';
}
ordertype.value =
botStore.activeBot.botState?.order_types?.forcebuy ||
botStore.activeBot.botState?.order_types?.force_entry ||
botStore.activeBot.botState?.order_types?.buy ||
botStore.activeBot.botState?.order_types?.entry ||
'limit';
};
const handleEntry = () => {

View File

@ -15,7 +15,6 @@
<span>Currently owning {{ trade.amount }} {{ trade.base_currency }}</span>
</p>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.12"
:label="`*Amount in ${trade.base_currency} [optional]`"
label-for="stake-input"
invalid-feedback="Amount must be empty or a positive number"
@ -40,7 +39,6 @@
</b-form-group>
<b-form-group
v-if="botStore.activeBot.botApiVersion > 1.1"
label="*OrderType"
label-for="ordertype-input"
invalid-feedback="OrderType"
@ -114,12 +112,10 @@ const handleSubmit = () => {
};
const resetForm = () => {
amount.value = props.trade.amount;
if (botStore.activeBot.botApiVersion > 1.1) {
ordertype.value =
botStore.activeBot.botState?.order_types?.force_exit ||
botStore.activeBot.botState?.order_types?.exit ||
'limit';
}
ordertype.value =
botStore.activeBot.botState?.order_types?.force_exit ||
botStore.activeBot.botState?.order_types?.exit ||
'limit';
};
const handleEntry = () => {

View File

@ -8,7 +8,9 @@
>
</b-form-select>
<div class="ms-2">
<b-button @click="botStore.activeBot.getFreqAIModelList">&#x21bb;</b-button>
<b-button @click="botStore.activeBot.getFreqAIModelList">
<i-mdi-refresh />
</b-button>
</div>
</div>
</div>

View File

@ -1,37 +1,29 @@
<template>
<div class="d-flex h-100 p-0 align-items-start">
<textarea v-model="formattedLogs" class="h-100" readonly></textarea>
<b-button id="refresh-logs" size="sm" @click="botStore.activeBot.getLogs">&#x21bb;</b-button>
<b-button id="refresh-logs" size="sm" @click="botStore.activeBot.getLogs">
<i-mdi-refresh />
</b-button>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { defineComponent, onMounted, computed } from 'vue';
import { onMounted, computed } from 'vue';
export default defineComponent({
name: 'LogViewer',
setup() {
const botStore = useBotStore();
const botStore = useBotStore();
onMounted(async () => {
botStore.activeBot.getLogs();
});
onMounted(async () => {
botStore.activeBot.getLogs();
});
const formattedLogs = computed(() => {
let result = '';
for (let i = 0, len = botStore.activeBot.lastLogs.length; i < len; i += 1) {
const log = botStore.activeBot.lastLogs[i];
result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`;
}
return result;
});
return {
botStore,
formattedLogs,
};
},
const formattedLogs = computed(() => {
let result = '';
for (let i = 0, len = botStore.activeBot.lastLogs.length; i < len; i += 1) {
const log = botStore.activeBot.lastLogs[i];
result += `${log[0]} - ${log[2]} - ${log[3]} - ${log[4]}\n`;
}
return result;
});
</script>

View File

@ -2,7 +2,9 @@
<div>
<div class="mb-2">
<label class="me-auto h3">Pair Locks</label>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getLocks">&#x21bb;</b-button>
<b-button class="float-end" size="sm" @click="botStore.activeBot.getLocks">
<i-mdi-refresh />
</b-button>
</div>
<div>
<b-table class="table-sm" :items="botStore.activeBot.activeLocks" :fields="tableFields">
@ -13,7 +15,7 @@
title="Delete trade"
@click="removePairLock(row.item)"
>
<DeleteIcon :size="16" />
<i-mdi-delete />
</b-button>
</template>
</b-table>
@ -21,45 +23,30 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { timestampms } from '@/shared/formatters';
import { Lock } from '@/types';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import { showAlert } from '@/stores/alerts';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { TableField } from 'bootstrap-vue-next';
const botStore = useBotStore();
export default defineComponent({
name: 'PairLockList',
components: { DeleteIcon },
setup() {
const botStore = useBotStore();
const tableFields: TableField[] = [
{ key: 'pair', label: 'Pair' },
{ key: 'lock_end_timestamp', label: 'Until', formatter: (value) => timestampms(value as number) },
{ key: 'reason', label: 'Reason' },
{ key: 'actions' },
];
const tableFields = [
{ key: 'pair', label: 'Pair' },
{ key: 'lock_end_timestamp', label: 'Until', formatter: 'timestampms' },
{ key: 'reason', label: 'Reason' },
{ key: 'actions' },
];
const removePairLock = (item: Lock) => {
console.log(item);
if (item.id !== undefined) {
botStore.activeBot.deleteLock(item.id);
} else {
showAlert('This Freqtrade version does not support deleting locks.');
}
};
return {
timestampms,
botStore,
tableFields,
removePairLock,
};
},
});
const removePairLock = (item: Lock) => {
console.log(item);
if (item.id !== undefined) {
botStore.activeBot.deleteLock(item.id);
} else {
showAlert('This Freqtrade version does not support deleting locks.');
}
};
</script>
<style scoped></style>

View File

@ -11,7 +11,7 @@
>
<div>
{{ comb.pair }}
<span v-if="comb.locks" :title="comb.lockReason"> &#128274; </span>
<span v-if="comb.locks" :title="comb.lockReason"> <i-mdi-lock /> </span>
</div>
<TradeProfit v-if="comb.trade && !backtestMode" :trade="comb.trade" />

View File

@ -12,34 +12,22 @@
title="Auto Refresh All bots"
@click="botStore.allRefreshFull"
>
<RefreshIcon :size="16" />
<i-mdi-refresh />
</b-button>
</div>
</template>
<script lang="ts">
import RefreshIcon from 'vue-material-design-icons/Refresh.vue';
import { defineComponent, computed } from 'vue';
<script setup lang="ts">
import { computed } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'ReloadControl',
components: { RefreshIcon },
setup() {
const botStore = useBotStore();
const autoRefreshLoc = computed({
get() {
return botStore.globalAutoRefresh;
},
set(newValue: boolean) {
botStore.setGlobalAutoRefresh(newValue);
},
});
return {
botStore,
autoRefreshLoc,
};
const botStore = useBotStore();
const autoRefreshLoc = computed({
get() {
return botStore.globalAutoRefresh;
},
set(newValue: boolean) {
botStore.setGlobalAutoRefresh(newValue);
},
});
</script>

View File

@ -8,7 +8,9 @@
>
</b-form-select>
<div class="ms-2">
<b-button @click="botStore.activeBot.getStrategyList">&#x21bb;</b-button>
<b-button @click="botStore.activeBot.getStrategyList">
<i-mdi-refresh />
</b-button>
</div>
</div>

View File

@ -7,62 +7,50 @@
></b-form-select>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
<script setup lang="ts">
import { computed, ref } from 'vue';
export default defineComponent({
name: 'TimefameSelect',
props: {
value: { default: '', type: String },
belowTimeframe: { required: false, default: '', type: String },
},
emits: ['input'],
setup(props, { emit }) {
const selectedTimeframe = ref('');
// The below list must always remain sorted correctly!
const availableTimeframesBase = [
// Placeholder value
{ value: '', text: 'Use strategy default' },
'1m',
'3m',
'5m',
'15m',
'30m',
'1h',
'2h',
'4h',
'6h',
'8h',
'12h',
'1d',
'3d',
'1w',
'2w',
'1M',
'1y',
];
const availableTimeframes = computed(() => {
if (!props.belowTimeframe) {
return availableTimeframesBase;
}
const idx = availableTimeframesBase.findIndex((v) => v === props.belowTimeframe);
return [...availableTimeframesBase].splice(0, idx);
});
const emitSelectedTimeframe = () => {
emit('input', selectedTimeframe.value);
};
return {
availableTimeframesBase,
availableTimeframes,
emitSelectedTimeframe,
selectedTimeframe,
};
},
const props = defineProps({
value: { default: '', type: String },
belowTimeframe: { required: false, default: '', type: String },
});
const emit = defineEmits(['input']);
const selectedTimeframe = ref('');
// The below list must always remain sorted correctly!
const availableTimeframesBase = [
// Placeholder value
{ value: '', text: 'Use strategy default' },
'1m',
'3m',
'5m',
'15m',
'30m',
'1h',
'2h',
'4h',
'6h',
'8h',
'12h',
'1d',
'3d',
'1w',
'2w',
'1M',
'1y',
];
const availableTimeframes = computed(() => {
if (!props.belowTimeframe) {
return availableTimeframesBase;
}
const idx = availableTimeframesBase.findIndex((v) => v === props.belowTimeframe);
return [...availableTimeframesBase].splice(0, idx);
});
const emitSelectedTimeframe = () => {
emit('input', selectedTimeframe.value);
};
</script>
<style scoped></style>

View File

@ -7,7 +7,7 @@
title="Forceexit"
@click="$emit('forceExit', trade)"
>
<ForceSellIcon :size="16" title="Forceexit" class="me-1" />Forceexit
<i-mdi-close-box class="me-1" />Forceexit
</b-button>
<b-button
v-if="botApiVersion > 1.1"
@ -16,7 +16,7 @@
title="Forceexit limit"
@click="$emit('forceExit', trade, 'limit')"
>
<ForceSellIcon :size="16" title="Forceexit limit" class="me-1" />Forceexit limit
<i-mdi-close-box class="me-1" />Forceexit limit
</b-button>
<b-button
v-if="botApiVersion > 1.1"
@ -25,7 +25,7 @@
title="Forceexit market"
@click="$emit('forceExit', trade, 'market')"
>
<ForceSellIcon :size="16" title="Forceexit market" class="me-1" />Forceexit market
<i-mdi-close-box class="me-1" />Forceexit market
</b-button>
<b-button
v-if="botApiVersion > 2.16"
@ -34,7 +34,7 @@
title="Forceexit partial"
@click="$emit('forceExitPartial', trade)"
>
<ForceSellPartialIcon :size="16" title="Forceexit partial" class="me-1" />Forceexit partial
<i-mdi-close-box-multiple class="me-1" />Forceexit partial
</b-button>
<b-button
v-if="botApiVersion >= 2.24 && trade.open_order_id"
@ -43,16 +43,24 @@
title="Cancel open orders"
@click="$emit('cancelOpenOrder', trade)"
>
<CancelIcon :size="16" title="Cancel open order" class="me-1" />Cancel open order
<i-mdi-cancel class="me-1" />Cancel open order
</b-button>
<b-button
v-if="botApiVersion >= 2.28"
class="btn-xs text-start mt-1"
size="sm"
title="Reload"
@click="$emit('reloadTrade', trade)"
>
<i-mdi-reload-alert class="me-1" />Reload Trade
</b-button>
<b-button
class="btn-xs text-start mt-1"
size="sm"
title="Delete trade"
@click="$emit('deleteTrade', trade)"
>
<DeleteIcon :size="16" title="Delete trade" class="me-1" />
<i-mdi-delete class="me-1" />
Delete
</b-button>
</div>
@ -60,10 +68,6 @@
<script setup lang="ts">
import { Trade } from '@/types';
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
import ForceSellPartialIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
import ForceSellIcon from 'vue-material-design-icons/CloseBox.vue';
import CancelIcon from 'vue-material-design-icons/Cancel.vue';
defineProps({
botApiVersion: {
@ -75,7 +79,7 @@ defineProps({
required: true,
},
});
defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'deleteTrade']);
defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'reloadTrade', 'deleteTrade']);
</script>
<style scoped lang="scss"></style>

View File

@ -1,30 +1,38 @@
<script setup lang="ts">
import ActionIcon from 'vue-material-design-icons/GestureTap.vue';
import TradeActions from './TradeActions.vue';
import CancelIcon from 'vue-material-design-icons/Cancel.vue';
import { Trade } from '@/types';
import { ref } from 'vue';
import TradeActions from './TradeActions.vue';
defineProps({
trade: { type: Object as () => Trade, required: true },
id: { type: Number, required: true },
botApiVersion: { type: Number, required: true },
});
const emit = defineEmits(['forceExit', 'forceExitPartial', 'cancelOpenOrder', 'deleteTrade']);
const emit = defineEmits([
'forceExit',
'forceExitPartial',
'cancelOpenOrder',
'reloadTrade',
'deleteTrade',
]);
const popoverOpen = ref(false);
const forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => {
function forceExitHandler(item: Trade, ordertype: string | undefined = undefined) {
popoverOpen.value = false;
emit('forceExit', item, ordertype);
};
const forceExitPartialHandler = (item: Trade) => {
}
function forceExitPartialHandler(item: Trade) {
popoverOpen.value = false;
emit('forceExitPartial', item);
};
const cancelOpenOrderHandler = (item: Trade) => {
}
function cancelOpenOrderHandler(item: Trade) {
popoverOpen.value = false;
emit('cancelOpenOrder', item);
};
}
function handleReloadTrade(item: Trade) {
popoverOpen.value = false;
emit('reloadTrade', item);
}
</script>
<template>
@ -36,7 +44,7 @@ const cancelOpenOrderHandler = (item: Trade) => {
title="Actions"
@click="popoverOpen = !popoverOpen"
>
<ActionIcon :size="16" title="Actions" />
<i-mdi-gesture-tap />
</b-button>
<b-popover
:target="`btn-actions-${id}`"
@ -55,9 +63,10 @@ const cancelOpenOrderHandler = (item: Trade) => {
$emit('deleteTrade', trade);
"
@cancel-open-order="cancelOpenOrderHandler"
@reload-trade="handleReloadTrade"
/>
<b-button class="mt-1 w-100 text-start" size="sm" @click="popoverOpen = false">
<CancelIcon :size="16" title="Close popup" class="me-1" />Close Actions menu
<i-mdi-cancel class="me-1" />Close Actions menu
</b-button>
</b-popover>
</div>

View File

@ -128,25 +128,16 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { formatPercent, formatPriceCurrency, formatPrice, timestampms } from '@/shared/formatters';
import ValuePair from '@/components/general/ValuePair.vue';
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
import { Trade } from '@/types';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'TradeDetail',
components: { ValuePair, TradeProfit, DateTimeTZ },
props: {
trade: { required: true, type: Object as () => Trade },
stakeCurrency: { required: true, type: String },
},
setup() {
return { timestampms, formatPercent, formatPrice, formatPriceCurrency };
},
defineProps({
trade: { required: true, type: Object as () => Trade },
stakeCurrency: { required: true, type: String },
});
</script>
<style scoped>

View File

@ -35,6 +35,7 @@
@force-exit="forceExitHandler"
@force-exit-partial="forceExitPartialHandler"
@cancel-open-order="cancelOpenOrderHandler"
@reload-trade="reloadTradeHandler"
/>
</template>
<template #cell(pair)="row">
@ -76,14 +77,9 @@
:per-page="perPage"
aria-controls="my-table"
></b-pagination>
<b-form-input
v-if="showFilter"
v-model="filterText"
type="text"
placeholder="Filter"
size="sm"
style="width: unset"
/>
<b-form-group v-if="showFilter" label-for="trade-filter">
<b-form-input id="trade-filter" v-model="filterText" type="text" placeholder="Filter" />
</b-form-group>
</div>
<force-exit-form v-if="activeTrades" v-model="forceExitVisible" :trade="feTrade" />
<b-modal v-model="removeTradeVisible" title="Exit trade" @ok="forceExitExecuter">
@ -246,6 +242,10 @@ const cancelOpenOrderHandler = (item: Trade) => {
removeTradeVisible.value = true;
};
function reloadTradeHandler(item: Trade) {
botStore.reloadTradeMulti({ tradeid: String(item.trade_id), botId: item.botId });
}
const handleContextMenuEvent = (item, index, event) => {
// stop browser context menu from appearing
if (!props.activeTrades) {

View File

@ -2,40 +2,34 @@
<span :title="timezoneTooltip">{{ formattedDate }}</span>
</template>
<script lang="ts">
<script setup lang="ts">
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
export default defineComponent({
name: 'DateTimeTZ',
props: {
date: { required: true, type: Number },
showTimezone: { required: false, type: Boolean, default: false },
dateOnly: { required: false, type: Boolean, default: false },
},
setup(props) {
const formattedDate = computed((): string => {
if (props.dateOnly) {
return timestampToDateString(props.date);
}
if (props.showTimezone) {
return timestampmsWithTimezone(props.date);
}
return timestampms(props.date);
});
const props = defineProps({
date: { required: true, type: Number },
showTimezone: { required: false, type: Boolean, default: false },
dateOnly: { required: false, type: Boolean, default: false },
});
const formattedDate = computed((): string => {
if (props.dateOnly) {
return timestampToDateString(props.date);
}
if (props.showTimezone) {
return timestampmsWithTimezone(props.date);
}
return timestampms(props.date);
});
const timezoneTooltip = computed((): string => {
const time1 = timestampmsWithTimezone(props.date);
const timeUTC = timestampmsWithTimezone(props.date, 'UTC');
if (time1 === timeUTC) {
return timeUTC;
}
const timezoneTooltip = computed((): string => {
const time1 = timestampmsWithTimezone(props.date);
const timeUTC = timestampmsWithTimezone(props.date, 'UTC');
if (time1 === timeUTC) {
return timeUTC;
}
return `${time1}\n${timeUTC}`;
});
return { formattedDate, timezoneTooltip };
},
return `${time1}\n${timeUTC}`;
});
</script>

View File

@ -0,0 +1,118 @@
<template>
<div class="d-flex flex-row">
<div class="flex-grow-1">
<slot v-if="!editing"> </slot>
<b-form-input v-else v-model="localName" size="sm"> </b-form-input>
</div>
<div
class="flex-grow-2 mt-auto d-flex gap-1 ms-1"
:class="alignVertical ? 'flex-column' : 'flex-row'"
>
<template v-if="allowEdit && !(addNew || editing)">
<b-button
size="sm"
variant="secondary"
:title="`Edit this ${editableName}.`"
@click="editing = true"
>
<i-mdi-pencil />
</b-button>
<b-button
size="sm"
variant="secondary"
:title="`Delete this ${editableName}.`"
@click="$emit('delete', modelValue)"
>
<i-mdi-delete />
</b-button>
</template>
<b-button
v-if="allowAdd && !(addNew || editing)"
size="sm"
:title="`Add new ${editableName}.`"
variant="primary"
@click="addNewClick"
><i-mdi-plus-box-outline />
</b-button>
<template v-if="addNew || editing">
<b-button
size="sm"
:title="`Add new '${editableName}`"
variant="primary"
@click="saveNewName"
>
<i-mdi-check />
</b-button>
<b-button size="sm" title="Abort" class="ms-1" variant="secondary" @click="abort">
<i-mdi-close />
</b-button>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps({
modelValue: {
type: String,
required: true,
},
allowEdit: {
type: Boolean,
default: false,
},
allowAdd: {
type: Boolean,
default: false,
},
editableName: {
type: String,
required: true,
},
alignVertical: {
type: Boolean,
default: false,
},
});
const emit = defineEmits<{
(e: 'delete', value: string): void;
(e: 'new', value: string): void;
(e: 'rename', oldName: string, newName: string): void;
}>();
const addNew = ref(false);
const localName = ref<string>(props.modelValue);
const editing = ref<boolean>(false);
function abort() {
editing.value = false;
addNew.value = false;
localName.value = props.modelValue;
}
function addNewClick() {
localName.value = '';
addNew.value = true;
editing.value = true;
}
watch(
() => props.modelValue,
() => {
localName.value = props.modelValue;
},
);
function saveNewName() {
editing.value = false;
if (addNew.value) {
addNew.value = false;
emit('new', localName.value);
} else {
// Editing
emit('rename', props.modelValue, localName.value);
}
}
</script>

View File

@ -1,11 +1,10 @@
<template>
<div :title="hint">
<InfoIcon :size="18" />
<i-mdi-information-outline />
</div>
</template>
<script setup>
import InfoIcon from 'vue-material-design-icons/InformationOutline.vue';
<script setup lang="ts">
defineProps({
hint: { type: String, required: true },
});

View File

@ -1,8 +1,12 @@
import ValuePair from '@/components/general/ValuePair.vue';
it('renders a message', () => {
const msg = 'Test description';
cy.mount(ValuePair, { props: { description: msg } });
cy.get('label').contains(msg);
describe('ValuePair.vue', () => {
it('Renders a message', () => {
const msg = 'Test description';
// https://github.com/cypress-io/cypress/issues/26628
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
cy.mount(ValuePair, { props: { description: msg } });
cy.get('label').contains(msg);
});
});

View File

@ -10,20 +10,14 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import InfoBox from '@/components/general/InfoBox.vue';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ValuePair',
components: { InfoBox },
props: {
description: { type: String, required: true },
help: { type: String, default: '', required: false },
classLabel: { type: String, default: 'col-4 fw-bold mb-0' },
classValue: { type: String, default: 'col-8' },
},
defineProps({
description: { type: String, required: true },
help: { type: String, default: '', required: false },
classLabel: { type: String, default: 'col-4 fw-bold mb-0' },
classValue: { type: String, default: 'col-8' },
});
</script>

View File

@ -3,49 +3,54 @@
<!-- Only visible on xs (phone) viewport! -->
<hr class="my-0" />
<div class="d-flex flex-align-center justify-content-between px-2">
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/open_trades">
<OpenTradesIcon />
<router-link
v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/open_trades"
>
<i-mdi-folder-open height="24" width="24" />
Trades
</router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/trade_history">
<ClosedTradesIcon />
<router-link
v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/trade_history"
>
<i-mdi-folder-lock height="24" width="24" />
History
</router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/pairlist">
<PairListIcon />
<router-link
v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/pairlist"
>
<i-mdi-view-list height="24" width="24" />
Pairlist
</router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/balance">
<BalanceIcon />
<router-link
v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/balance"
>
<i-mdi-bank height="24" width="24" />
Balance
</router-link>
<router-link v-if="!botStore.canRunBacktest" class="nav-link navbar-nav" to="/dashboard">
<DashboardIcon />
<router-link
v-if="!botStore.canRunBacktest"
class="nav-link navbar-nav align-items-center"
to="/dashboard"
>
<i-mdi-view-dashboard-outline height="24" width="24" />
Dashboard
</router-link>
</div>
</footer>
</template>
<script lang="ts">
import OpenTradesIcon from 'vue-material-design-icons/FolderOpen.vue';
import ClosedTradesIcon from 'vue-material-design-icons/FolderLock.vue';
import BalanceIcon from 'vue-material-design-icons/Bank.vue';
import PairListIcon from 'vue-material-design-icons/ViewList.vue';
import DashboardIcon from 'vue-material-design-icons/ViewDashboardOutline.vue';
import { defineComponent } from 'vue';
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'NavFooter',
components: { OpenTradesIcon, ClosedTradesIcon, BalanceIcon, PairListIcon, DashboardIcon },
setup() {
const botStore = useBotStore();
return {
botStore,
};
},
});
const botStore = useBotStore();
</script>
<style lang="scss" scoped>

View File

@ -2,7 +2,8 @@ import { createPinia, PiniaVuePlugin } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { createApp } from 'vue';
import App from './App.vue';
import { BootstrapVue3 } from './plugins/bootstrap-vue';
// Eensure Bootstrap css still loads
import './plugins/bootstrap-vue';
import { GridLayout } from './plugins/vue-grid-layout';
import router from './router';
@ -14,7 +15,6 @@ pinia.use(piniaPluginPersistedstate);
myApp.use(pinia);
myApp.use(router);
myApp.use(BootstrapVue3);
myApp.use(GridLayout);
// Vue.config.productionTip = false;

View File

@ -1,7 +1,4 @@
import BootstrapVue3 from 'bootstrap-vue-next';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
import '@/styles/main.scss';
export { BootstrapVue3 };

View File

@ -18,7 +18,7 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/graph',
name: 'Freqtrade Graph',
component: () => import('@/views/GraphsView.vue'),
component: () => import('@/views/ChartsView.vue'),
},
{
path: '/logs',

View File

@ -0,0 +1,50 @@
import { PlotConfig } from '@/types';
/**
* Calculate diff over 2 dataset columns and adds it to the end of the dataset
* modifies the incomming dataset array inplace!
*/
export function calculateDiff(
columns: string[],
data: number[][],
colFrom: string,
colTo: string,
): number[][] {
const fromIdx = columns.indexOf(colFrom);
const toIdx = columns.indexOf(colTo);
columns.push(`${colFrom}-${colTo}`);
return data.map((original) => {
// Prevent mutation of original data
const candle = original.slice();
const diff =
candle === null || candle[toIdx] === null || candle[fromIdx] === null
? null
: candle[toIdx] - candle[fromIdx];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
candle.push(diff);
return candle;
});
}
export function getDiffColumnsFromPlotConfig(plotConfig: PlotConfig): string[][] {
const result: string[][] = [];
if ('main_plot' in plotConfig) {
Object.entries(plotConfig.main_plot).forEach(([key, value]) => {
if (value.fill_to) {
result.push([key, value.fill_to]);
}
});
}
if ('subplots' in plotConfig) {
Object.values(plotConfig.subplots).forEach((subplots) => {
Object.entries(subplots).forEach(([key, value]) => {
if (value.fill_to) {
result.push([key, value.fill_to]);
}
});
});
}
return result;
}

View File

@ -0,0 +1,69 @@
import { ChartType, IndicatorConfig } from '@/types';
import { BarSeriesOption, LineSeriesOption, ScatterSeriesOption } from 'echarts';
import randomColor from '../randomColor';
export type SupportedSeriesTypes = LineSeriesOption | BarSeriesOption | ScatterSeriesOption;
export function generateCandleSeries(
colDate: number,
col: number,
key: string,
value: IndicatorConfig,
axis = 0,
): SupportedSeriesTypes {
const sp: SupportedSeriesTypes = {
name: key,
type: value.type || 'line',
xAxisIndex: axis,
yAxisIndex: axis,
itemStyle: {
color: value.color || randomColor(),
},
encode: {
x: colDate,
y: col,
},
showSymbol: false,
};
return sp;
}
export function generateAreaCandleSeries(
colDate: number,
fillCol: number,
key: string,
value: IndicatorConfig,
axis = 0,
): SupportedSeriesTypes {
const fillValue: IndicatorConfig = {
// color: value.color;
type: ChartType.line,
};
const areaSeries = generateCandleSeries(
colDate,
fillCol,
key,
fillValue,
axis,
) as LineSeriesOption;
const areaOptions: LineSeriesOption = {
stack: key,
stackStrategy: 'all',
lineStyle: {
opacity: 0,
},
showSymbol: false,
areaStyle: {
color: value.color,
opacity: 0.1,
},
tooltip: {
show: false, // hide value on tooltip
},
};
Object.assign(areaSeries, areaOptions);
return areaSeries;
}

View File

@ -1,4 +1,4 @@
export default function heikinAshiDataset(columns: string[], data: Array<number[]>) {
export function heikinAshiDataset(columns: string[], data: Array<number[]>): number[][] {
const openIdx = columns.indexOf('open');
const closeIdx = columns.indexOf('close');
const highIdx = columns.indexOf('high');
@ -26,3 +26,5 @@ export default function heikinAshiDataset(columns: string[], data: Array<number[
return candle;
});
}
export default heikinAshiDataset;

3
src/shared/deepClone.ts Normal file
View File

@ -0,0 +1,3 @@
export function deepClone<T>(object: T): T {
return JSON.parse(JSON.stringify(object));
}

View File

@ -1,35 +0,0 @@
import { PlotConfig, EMPTY_PLOTCONFIG, PlotConfigStorage } from '@/types';
const PLOT_CONFIG = 'ft_custom_plot_config';
const PLOT_CONFIG_NAME = 'ft_selected_plot_config';
export function getPlotConfigName(): string {
return localStorage.getItem(PLOT_CONFIG_NAME) || 'default';
}
export function storePlotConfigName(plotConfigName: string): void {
localStorage.setItem(PLOT_CONFIG_NAME, plotConfigName);
}
export function getAllCustomPlotConfig(): PlotConfig {
return JSON.parse(localStorage.getItem(PLOT_CONFIG) || '{}');
}
export function getAllPlotConfigNames(): Array<string> {
return Object.keys(getAllCustomPlotConfig());
}
export function getCustomPlotConfig(configName: string): PlotConfig {
const configs = getAllCustomPlotConfig();
return configName in configs ? configs[configName] : { ...EMPTY_PLOTCONFIG };
}
export function storeCustomPlotConfig(plotConfig: PlotConfigStorage) {
const existingConfig = getAllCustomPlotConfig();
// Merge existing with new config
const finalPlotConfig = { ...existingConfig, ...plotConfig };
localStorage.setItem(PLOT_CONFIG, JSON.stringify(finalPlotConfig));
// Store new config name as default
storePlotConfigName(Object.keys(plotConfig)[0]);
}

View File

@ -99,14 +99,17 @@ export class UserService {
public static getAvailableBots(): BotDescriptors {
const allInfo = UserService.getAllLoginInfos();
const response: BotDescriptors = {};
Object.entries(allInfo).forEach(([k, v], idx) => {
response[k] = {
botId: k,
botName: v.botName,
botUrl: v.apiUrl,
sortId: v.sortId ?? idx,
};
});
Object.keys(allInfo)
.sort((a, b) => (allInfo[a].sortId ?? 0) - (allInfo[b].sortId ?? 0))
.forEach((k, idx) => {
response[k] = {
botId: k,
botName: allInfo[k].botName,
botUrl: allInfo[k].apiUrl,
sortId: allInfo[k].sortId ?? idx,
};
});
return response;
}

View File

@ -520,7 +520,7 @@ export function createBotSubStore(botId: string, botName: string) {
},
async getState() {
try {
const { data } = await api.get('/show_config');
const { data } = await api.get<BotState>('/show_config');
this.botState = data;
this.botStatusAvailable = true;
this.startWebSocket();
@ -654,6 +654,18 @@ export function createBotSubStore(botId: string, botName: string) {
return Promise.reject(error);
}
},
async reloadTrade(tradeid: string) {
try {
const res = await api.post<never, AxiosResponse<Trade>>(`/trades/${tradeid}/reload`);
return Promise.resolve(res);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.response);
}
showAlert(`Failed to reload trade ${tradeid}`, 'danger');
return Promise.reject(error);
}
},
async startTrade() {
try {
const res = await api.post('/start_trade', {});
@ -692,10 +704,7 @@ export function createBotSubStore(botId: string, botName: string) {
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.response);
showAlert(
`Error occured entering: '${(error as any).response?.data?.error}'`,
'danger',
);
showAlert(`Error occured entering: '${error.response?.data?.error}'`, 'danger');
}
return Promise.reject(error);
}
@ -730,9 +739,7 @@ export function createBotSubStore(botId: string, botName: string) {
if (axios.isAxiosError(error)) {
console.error(error.response);
showAlert(
`Error occured while adding pairs to Blacklist: '${
(error as any).response?.data?.error
}'`,
`Error occured while adding pairs to Blacklist: '${error.response?.data?.error}'`,
'danger',
);
}
@ -778,9 +785,7 @@ export function createBotSubStore(botId: string, botName: string) {
if (axios.isAxiosError(error)) {
console.error(error.response);
showAlert(
`Error occured while removing pairs from Blacklist: '${
(error as any).response?.data?.error
}'`,
`Error occured while removing pairs from Blacklist: '${error.response?.data?.error}'`,
'danger',
);
}
@ -876,6 +881,7 @@ export function createBotSubStore(botId: string, botName: string) {
return Promise.reject(err);
}
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_handleWebsocketMessage(ws, event: MessageEvent<any>) {
const msg: FTWsMessage = JSON.parse(event.data);
switch (msg.type) {
@ -899,6 +905,7 @@ export function createBotSubStore(botId: string, botName: string) {
}
default:
// Unhandled events ...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.log(`Received event ${(msg as any).type}`);
break;
}

View File

@ -8,8 +8,10 @@ import {
DailyPayload,
DailyRecord,
DailyReturnValue,
MultiCancelOpenOrderPayload,
MultiDeletePayload,
MultiForcesellPayload,
MultiReloadTradePayload,
ProfitInterface,
Trade,
} from '@/types';
@ -234,7 +236,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
// Ensure all bots status is correct.
await this.pingAll();
const botStoreUpdates: Promise<any>[] = [];
const botStoreUpdates: Promise<BotState>[] = [];
this.allBotStores.forEach((bot) => {
if (bot.isBotOnline && !bot.botStatusAvailable) {
botStoreUpdates.push(bot.getState());
@ -283,7 +285,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
},
async pingAll() {
await Promise.all(
Object.entries(this.botStores).map(async ([_, v]) => {
Object.values(this.botStores).map(async (v) => {
try {
await v.fetchPing();
} catch {
@ -293,7 +295,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
);
},
allGetState() {
Object.entries(this.botStores).map(async ([_, v]) => {
Object.values(this.botStores).map(async (v) => {
try {
await v.getState();
} catch {
@ -317,9 +319,12 @@ export const useBotStore = defineStore('ftbot-wrapper', {
async deleteTradeMulti(deletePayload: MultiDeletePayload) {
return this.botStores[deletePayload.botId].deleteTrade(deletePayload.tradeid);
},
async cancelOpenOrderMulti(deletePayload: MultiDeletePayload) {
async cancelOpenOrderMulti(deletePayload: MultiCancelOpenOrderPayload) {
return this.botStores[deletePayload.botId].cancelOpenOrder(deletePayload.tradeid);
},
async reloadTradeMulti(deletePayload: MultiReloadTradePayload) {
return this.botStores[deletePayload.botId].reloadTrade(deletePayload.tradeid);
},
},
});

View File

@ -1,43 +1,80 @@
import {
getAllPlotConfigNames,
getCustomPlotConfig,
getPlotConfigName,
storeCustomPlotConfig,
storePlotConfigName,
} from '@/shared/storage';
import { PlotConfigStorage, EMPTY_PLOTCONFIG, PlotConfig } from '@/types';
import { deepClone } from '@/shared/deepClone';
import { EMPTY_PLOTCONFIG, PlotConfig, PlotConfigStorage } from '@/types';
import { defineStore } from 'pinia';
const FT_PLOT_CONFIG_KEY = 'ftPlotConfig';
function migratePlotConfigs() {
// Legacy config names
const PLOT_CONFIG = 'ft_custom_plot_config';
const PLOT_CONFIG_NAME = 'ft_selected_plot_config';
const allConfigs = JSON.parse(localStorage.getItem(PLOT_CONFIG) || '{}');
if (Object.keys(allConfigs).length > 0) {
console.log('migrating plot configs');
const res = {
customPlotConfigs: allConfigs,
plotConfigName: localStorage.getItem(PLOT_CONFIG_NAME) || 'default',
};
localStorage.setItem(FT_PLOT_CONFIG_KEY, JSON.stringify(res));
localStorage.removeItem(PLOT_CONFIG);
localStorage.removeItem(PLOT_CONFIG_NAME);
}
}
migratePlotConfigs();
export const usePlotConfigStore = defineStore('plotConfig', {
state: () => {
return {
customPlotConfig: {} as PlotConfigStorage,
plotConfigName: getPlotConfigName(),
availablePlotConfigNames: getAllPlotConfigNames(),
plotConfig: { ...EMPTY_PLOTCONFIG },
customPlotConfigs: {} as PlotConfigStorage,
plotConfigName: 'default',
isEditing: false,
editablePlotConfig: { ...EMPTY_PLOTCONFIG } as PlotConfig,
};
},
getters: {
availablePlotConfigNames: (state) => Object.keys(state.customPlotConfigs),
plotConfig: (state) =>
(state.isEditing
? state.editablePlotConfig
: state.customPlotConfigs[state.plotConfigName]) || deepClone(EMPTY_PLOTCONFIG),
// plotConfig: (state) => state.customPlotConfig[state.plotConfigName] || { ...EMPTY_PLOTCONFIG },
},
actions: {
saveCustomPlotConfig(plotConfig: PlotConfigStorage) {
this.customPlotConfig = plotConfig;
storeCustomPlotConfig(plotConfig);
this.availablePlotConfigNames = getAllPlotConfigNames();
saveCustomPlotConfig(name: string, plotConfig: PlotConfig) {
// This will autosave to storage due to pinia-persist
this.customPlotConfigs[name] = plotConfig;
},
setPlotConfigName(plotConfigName: string) {
deletePlotConfig(plotConfigName: string) {
delete this.customPlotConfigs[plotConfigName];
if (this.plotConfigName === plotConfigName) {
this.plotConfigName =
this.availablePlotConfigNames[this.availablePlotConfigNames.length - 1];
}
},
renamePlotConfig(oldName: string, newName: string) {
this.customPlotConfigs[newName] = this.customPlotConfigs[oldName];
delete this.customPlotConfigs[oldName];
this.plotConfigName = newName;
},
newPlotConfig(plotConfigName: string) {
this.editablePlotConfig = deepClone(EMPTY_PLOTCONFIG);
this.saveCustomPlotConfig(plotConfigName, this.editablePlotConfig);
this.plotConfigName = plotConfigName;
storePlotConfigName(plotConfigName);
},
plotConfigChanged(plotConfigName = '') {
console.log('plotConfigChanged');
this.setPlotConfigName(plotConfigName ? plotConfigName : this.plotConfigName);
this.plotConfig = getCustomPlotConfig(this.plotConfigName);
},
setPlotConfig(plotConfig: PlotConfig) {
console.log('emit...');
this.plotConfig = { ...plotConfig };
if (plotConfigName) {
this.plotConfigName = plotConfigName;
}
console.log('plotConfigChanged', this.plotConfigName);
if (this.isEditing) {
this.editablePlotConfig = deepClone(this.customPlotConfigs[this.plotConfigName]);
}
},
},
persist: {
key: FT_PLOT_CONFIG_KEY,
paths: ['plotConfigName', 'customPlotConfigs'],
},
});

View File

@ -13,7 +13,7 @@
}
.v-select * {
font-size: 0.8rem;
font-size: 0.8rem !important;
}
.modal.show {
@ -22,13 +22,21 @@
}
.btn-primary {
color: #ffffff
color: #ffffff !important;
}
.text-bg-primary {
color: #ffffff !important;
}
.card {
padding: 0px;
}
.vs__open-indicator {
transform: none !important;
}
[data-theme="dark"] {
$bg-dark: rgb(18, 18, 18);
@ -133,8 +141,9 @@
}
// Styles for searchable select
.vs__dropdown-toggle {
.vs__dropdown-toggle, .vs__clear {
border-color: lighten($bg-dark, 20%);
color: $fg-color;
// border: 1px solid $fg-color;
}
@ -189,7 +198,11 @@
.form-select {
color: $fg-color;
border-color: lighten($bg-dark, 20%);
background: $bg-dark url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23dedede' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px
background: $bg-dark;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dedede' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
background-size: 16px 12px;
background-repeat: no-repeat;
background-position: right 0.75rem center;
}
.b-toast .toast {

View File

@ -50,14 +50,32 @@ export interface ExitReasonResults {
wins: number;
}
// Generated by https://quicktype.io
export interface PeriodicStat {
date: string;
date_ts: number;
profit_abs: number;
wins: number;
draws: number;
loses: number;
}
export interface PeriodicBreakdown {
day: PeriodicStat[];
week: PeriodicStat[];
month: PeriodicStat[];
}
export interface StrategyBacktestResult {
trades: ClosedTrade[];
locks: Lock[];
best_pair: PairResult;
worst_pair: PairResult;
results_per_pair: Array<PairResult>;
sell_reason_summary?: Array<ExitReasonResults>;
exit_reason_summary?: Array<ExitReasonResults>;
results_per_pair: PairResult[];
sell_reason_summary?: ExitReasonResults[];
exit_reason_summary?: ExitReasonResults[];
periodic_breakdown?: PeriodicBreakdown;
left_open_trades: Trade[];
total_trades: number;
total_volume: number;

View File

@ -1,17 +1,18 @@
export interface BalanceRecords {
[key: string]: string | number;
balance: number;
currency: string;
est_stake: number;
est_stake_bot?: number;
free: number;
used: number;
bot_owned?: number;
stake: string;
// Properties added in v 2.x
// Temporarily disabled to fix type errors
// side: string;
// leverage: number;
// is_position: boolean;
// position: number;
side: string;
leverage: number;
is_position: boolean;
position: number;
is_bot_managed?: boolean;
}
export interface BalanceInterface {
@ -21,10 +22,14 @@ export interface BalanceInterface {
stake: string;
/** Fiat symbol used */
symbol: string;
/** Total Balance in stake currency */
/** Total Account Balance in stake currency */
total: number;
/** Balance in FIAT currency */
/** Total Bot Balance in stake currency */
total_bot?: number;
/** Account Balance in FIAT currency */
value: number;
/** Bot Balance in FIAT currency */
value_bot?: number;
/** Assumed starting capital */
starting_capital: number;
/** Change between starting capital and current value */
@ -36,3 +41,13 @@ export interface BalanceInterface {
starting_capital_fiat_ratio: number;
starting_capital_fiat_pct: number;
}
export interface BalanceValues {
[key: string]: number | string;
balance: number;
currency: string;
est_stake: number;
free: number;
used: number;
stake: string;
}

View File

@ -7,6 +7,12 @@ export interface CumProfitDataPerDate {
[key: number]: CumProfitData;
}
export type CumProfitChartData = {
date: number;
profit?: number;
currentProfit?: number;
};
export interface ChartSliderPosition {
startValue: number;
endValue: number | undefined;

View File

@ -7,6 +7,7 @@ export enum ChartType {
export interface IndicatorConfig {
color?: string;
type?: ChartType;
fill_to?: string;
}
export interface PlotConfig {

View File

@ -26,8 +26,10 @@ export interface ProfitInterface {
trade_count: number;
closed_trade_count: number;
first_trade_date: string;
first_trade_humanized: string;
first_trade_timestamp: number;
latest_trade_date: string;
latest_trade_humanized: string;
latest_trade_timestamp: number;
avg_duration: string;
best_pair: string;

View File

@ -134,7 +134,7 @@ export interface ClosedTrade extends TradeBase {
fee_close_cost?: number;
fee_close_currency?: string;
sell_reason: string;
exit_reason: string;
min_rate: number;
max_rate: number;
}

View File

@ -25,11 +25,15 @@ export interface MultiForcesellPayload extends ForceSellPayload {
}
/** Interface only used internally to ensure the right bot is being called in a multibot environment. */
export interface MultiDeletePayload {
export interface MultiBotIdPayload {
tradeid: string;
botId: string;
}
export type MultiDeletePayload = MultiBotIdPayload;
export type MultiReloadTradePayload = MultiBotIdPayload;
export type MultiCancelOpenOrderPayload = MultiBotIdPayload;
export interface PerformanceEntry {
count: number;
pair: string;
@ -121,8 +125,7 @@ export interface EntryPricing extends PriceBase {
export interface BotState {
version: string;
strategy_version?: string;
/** Api version - was not provided prior to 1.1 (or 2021.11) */
api_version?: number;
api_version: number;
dry_run: boolean;
/** Futures, margin or spot */
trading_mode?: TradingMode;
@ -136,8 +139,6 @@ export interface BotState {
unfilledtimeout: UnfilledTimeout;
order_types: OrderTypes;
exchange: string;
/** @deprecated replaced by force_entry_enable in 2.x */
forcebuy_enabled?: boolean;
force_entry_enable?: boolean;
max_open_trades: number;
minimal_roi: object;
@ -206,6 +207,7 @@ export interface PairHistoryPayload {
timeframe: string;
timerange: string;
strategy: string;
freqaimodel?: string;
}
export interface PairHistory {
@ -214,7 +216,7 @@ export interface PairHistory {
timeframe: string;
timeframe_ms: number;
columns: string[];
data: Array<number[]>;
data: number[][];
length: number;
/** Number of buy signals in this response */
buy_signals: number;

View File

@ -316,6 +316,7 @@
:timerange="timerange"
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
:trades="botStore.activeBot.selectedBacktestResult.trades"
:freqai-model="freqAI.enabled ? freqAI.model : undefined"
/>
</div>
</div>

View File

@ -86,7 +86,11 @@
drag-allow-from=".drag-header"
>
<DraggableContainer header="Cumulative Profit">
<CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
<CumProfitChart
:trades="botStore.allTradesSelectedBots"
:open-trades="botStore.allOpenTradesSelectedBots"
:show-title="false"
/>
</DraggableContainer>
</grid-item>
<grid-item

View File

@ -8,7 +8,3 @@
</p>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -4,14 +4,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import LogViewer from '@/components/ftbot/LogViewer.vue';
export default defineComponent({
name: 'LogView',
components: { LogViewer },
});
</script>
<style scoped></style>

View File

@ -1,8 +1,6 @@
<template>
<div>
<b-button @click="openLoginModal()"
><LoginIcon :size="16" class="me-1" />{{ loginText }}</b-button
>
<b-button @click="openLoginModal()"><i-mdi-login class="me-1" />{{ loginText }}</b-button>
<b-modal
id="modal-prevent-closing"
v-model="loginViewOpen"
@ -23,7 +21,6 @@
import Login from '@/components/BotLogin.vue';
import { AuthStorageWithBotId } from '@/types';
import { nextTick, ref } from 'vue';
import LoginIcon from 'vue-material-design-icons/Login.vue';
defineProps({
loginText: { required: false, default: 'Login', type: String },

View File

@ -1,13 +1,13 @@
<template>
<div class="container">
<b-card header="Freqtrade bot Login">
<Login ref="loginForm" />
<BotLogin ref="loginForm" />
</b-card>
</div>
</template>
<script setup lang="ts">
import Login from '@/components/BotLogin.vue';
import BotLogin from '@/components/BotLogin.vue';
</script>
<style scoped>

View File

@ -30,7 +30,7 @@
size="sm"
class="align-self-start mt-1 ms-1"
@click="botStore.activeBot.setDetailTrade(null)"
><BackIcon /> Back</b-button
><i-mdi-arrow-left /> Back</b-button
>
<TradeDetail
:trade="botStore.activeBot.tradeDetail"
@ -40,31 +40,15 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import CustomTradeList from '@/components/ftbot/CustomTradeList.vue';
import TradeDetail from '@/components/ftbot/TradeDetail.vue';
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import { defineComponent } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
export default defineComponent({
name: 'TradesList',
components: {
CustomTradeList,
TradeDetail,
BackIcon,
},
props: {
history: { default: false, type: Boolean },
},
setup() {
const botStore = useBotStore();
return {
botStore,
};
},
defineProps({
history: { default: false, type: Boolean },
});
const botStore = useBotStore();
</script>
<style scoped></style>

View File

@ -16,6 +16,7 @@
"types": [
"cypress",
"vite/client",
"unplugin-icons/types/vue",
],
"paths": {
"@/*": [

View File

@ -1,10 +1,24 @@
import { defineConfig } from 'vite';
import createVuePlugin from '@vitejs/plugin-vue';
import { resolve } from 'path';
import Icons from 'unplugin-icons/vite';
import Components from 'unplugin-vue-components/vite';
import IconsResolve from 'unplugin-icons/resolver';
import { BootstrapVueNextResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [createVuePlugin({})],
plugins: [
createVuePlugin({}),
Components({
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
dirs: [],
dts: true,
}),
Icons({
compiler: 'vue3',
}),
],
resolve: {
dedupe: ['vue'],
alias: {
@ -29,6 +43,7 @@ export default defineConfig({
changeOrigin: true,
},
},
host: '127.0.0.1',
port: 3000,
},
});

1023
yarn.lock

File diff suppressed because it is too large Load Diff