mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-27 13:35:17 +00:00
Merge branch 'main' into pr/qiweiii/1363
This commit is contained in:
commit
e39c436b67
|
@ -30,6 +30,7 @@ module.exports = {
|
||||||
// disable eslint no-shadow as it's causing false positives on typescript enums
|
// disable eslint no-shadow as it's causing false positives on typescript enums
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': ['error'],
|
'prettier/prettier': ['error'],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
// '@typescript-eslint/naming-convention': [
|
// '@typescript-eslint/naming-convention': [
|
||||||
// 'error',
|
// 'error',
|
||||||
// {
|
// {
|
||||||
|
|
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -7,6 +7,11 @@ assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**FreqUI Version**
|
||||||
|
|
||||||
|
- Version of freqUI: _____ (Available in the top right corner of the UI)
|
||||||
|
- version of freqtrade: _____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker)
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
|
@ -23,7 +23,7 @@ updates:
|
||||||
day: "wednesday"
|
day: "wednesday"
|
||||||
time: "03:00"
|
time: "03:00"
|
||||||
timezone: "UTC"
|
timezone: "UTC"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 20
|
||||||
target-branch: main
|
target-branch: main
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
|
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -21,10 +21,10 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-22.04 ]
|
os: [ ubuntu-22.04 ]
|
||||||
node: [ "16", "18", "19", "20"]
|
node: [ "16", "18", "20"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
|
@ -42,7 +42,7 @@ jobs:
|
||||||
run: yarn test:unit
|
run: yarn test:unit
|
||||||
|
|
||||||
- name: Run Component tests
|
- name: Run Component tests
|
||||||
uses: cypress-io/github-action@v5
|
uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
# we have already installed everything
|
# we have already installed everything
|
||||||
install: false
|
install: false
|
||||||
|
@ -50,7 +50,7 @@ jobs:
|
||||||
command: yarn cypress run --component
|
command: yarn cypress run --component
|
||||||
|
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@v5
|
uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
# build: yarn build
|
# build: yarn build
|
||||||
start: yarn serve --host
|
start: yarn serve --host
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:20.4.0-alpine as ui-builder
|
FROM node:20.6.1-alpine as ui-builder
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ COPY . /app
|
||||||
# webpack and node17
|
# webpack and node17
|
||||||
RUN NODE_OPTIONS=--openssl-legacy-provider yarn build
|
RUN NODE_OPTIONS=--openssl-legacy-provider yarn build
|
||||||
|
|
||||||
FROM nginx:1.25.1-alpine
|
FROM nginx:1.25.2-alpine
|
||||||
COPY --from=ui-builder /app/dist /etc/nginx/html
|
COPY --from=ui-builder /app/dist /etc/nginx/html
|
||||||
COPY --from=ui-builder /app/nginx.conf /etc/nginx/nginx.conf
|
COPY --from=ui-builder /app/nginx.conf /etc/nginx/nginx.conf
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
|
@ -9,6 +9,9 @@ function tradeMocks() {
|
||||||
cy.intercept('GET', '**/api/v1/blacklist', { fixture: 'blacklist.json' }).as('Blacklist');
|
cy.intercept('GET', '**/api/v1/blacklist', { fixture: 'blacklist.json' }).as('Blacklist');
|
||||||
cy.intercept('GET', '**/api/v1/locks', { fixture: 'locks_empty.json' }).as('Locks');
|
cy.intercept('GET', '**/api/v1/locks', { fixture: 'locks_empty.json' }).as('Locks');
|
||||||
cy.intercept('GET', '**/api/v1/performance', { fixture: 'performance.json' }).as('Performance');
|
cy.intercept('GET', '**/api/v1/performance', { fixture: 'performance.json' }).as('Performance');
|
||||||
|
cy.intercept('POST', '**/api/v1/reload_config', { fixture: 'reload_config.json' }).as(
|
||||||
|
'ReloadConfig',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Trade', () => {
|
describe('Trade', () => {
|
||||||
|
@ -60,5 +63,18 @@ describe('Trade', () => {
|
||||||
cy.get('.drag-header').contains('Closed Trades').scrollIntoView().should('be.visible');
|
cy.get('.drag-header').contains('Closed Trades').scrollIntoView().should('be.visible');
|
||||||
cy.get('span').contains('TRX/USDT').should('be.visible');
|
cy.get('span').contains('TRX/USDT').should('be.visible');
|
||||||
cy.get('td').contains('8070.5').should('be.visible');
|
cy.get('td').contains('8070.5').should('be.visible');
|
||||||
|
// Scroll to top
|
||||||
|
cy.contains('Multi Pane').scrollIntoView().should('be.visible');
|
||||||
|
cy.get('button[title*="Reload Config "]').click();
|
||||||
|
|
||||||
|
// Reload Modal open
|
||||||
|
cy.get('.modal-dialog > .modal-content > .modal-footer > .btn-primary')
|
||||||
|
.filter(':visible')
|
||||||
|
.contains('Ok')
|
||||||
|
.should('be.visible')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Alert is visible
|
||||||
|
cy.contains('Config reloaded successfully.').scrollIntoView().should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
"progress": 1.0,
|
"progress": 1.0,
|
||||||
"trade_count": null,
|
"trade_count": null,
|
||||||
"backtest_result": {
|
"backtest_result": {
|
||||||
|
"metadata": {
|
||||||
|
"SampleStrategy": {
|
||||||
|
"strategy": "SampleStrategy",
|
||||||
|
"run_id": "0afc3cf414b6da90210f458078a6b06055b0c4e7",
|
||||||
|
"filename": "backtest-result-2023-08-03_04-02-20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"strategy": {
|
"strategy": {
|
||||||
"SampleStrategy": {
|
"SampleStrategy": {
|
||||||
"trades": [
|
"trades": [
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"closed_trade_count": 149,
|
"closed_trade_count": 149,
|
||||||
"first_trade_date": "a year ago",
|
"first_trade_date": "a year ago",
|
||||||
"first_trade_timestamp": 1626021644985,
|
"first_trade_timestamp": 1626021644985,
|
||||||
|
"bot_start_timestamp": 1625021644985,
|
||||||
"latest_trade_date": "5 days ago",
|
"latest_trade_date": "5 days ago",
|
||||||
"latest_trade_timestamp": 1661828800529,
|
"latest_trade_timestamp": 1661828800529,
|
||||||
"avg_duration": "7:59:54",
|
"avg_duration": "7:59:54",
|
||||||
|
|
3
cypress/fixtures/reload_config.json
Normal file
3
cypress/fixtures/reload_config.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"status": "Config reloaded successfully."
|
||||||
|
}
|
62
package.json
62
package.json
|
@ -17,55 +17,55 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@vuepic/vue-datepicker": "^5.4.0",
|
"@vuepic/vue-datepicker": "^6.1.0",
|
||||||
"@vueuse/core": "^10.2.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
"@vueuse/integrations": "^10.2.1",
|
"@vueuse/integrations": "^10.4.1",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.5.0",
|
||||||
"bootstrap": "^5.3.0",
|
"bootstrap": "^5.3.1",
|
||||||
"bootstrap-vue-next": "^0.8.13",
|
"bootstrap-vue-next": "^0.12.3",
|
||||||
"core-js": "^3.31.1",
|
"core-js": "^3.32.2",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"date-fns-tz": "^2.0.0",
|
"date-fns-tz": "^2.0.0",
|
||||||
"echarts": "^5.4.2",
|
"echarts": "^5.4.3",
|
||||||
"favico.js": "^0.3.10",
|
"favico.js": "^0.3.10",
|
||||||
"humanize-duration": "^3.29.0",
|
"humanize-duration": "^3.29.0",
|
||||||
"pinia": "^2.1.4",
|
"pinia": "^2.1.6",
|
||||||
"pinia-plugin-persistedstate": "^3.1.0",
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"vue": "^3.3.2",
|
"vue": "^3.3.2",
|
||||||
"vue-class-component": "^7.2.5",
|
"vue-class-component": "^7.2.5",
|
||||||
"vue-demi": "^0.14.1",
|
"vue-demi": "^0.14.6",
|
||||||
"vue-echarts": "^6.6.0",
|
"vue-echarts": "^6.6.1",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"vue-select": "^4.0.0-beta.6",
|
"vue-select": "^4.0.0-beta.6",
|
||||||
"vue3-drr-grid-layout": "^1.9.7"
|
"vue3-drr-grid-layout": "^1.9.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/vite-dev-server": "^5.0.5",
|
"@cypress/vite-dev-server": "^5.0.6",
|
||||||
"@cypress/vue": "^5.0.5",
|
"@cypress/vue": "^6.0.0",
|
||||||
"@iconify-json/mdi": "^1.1.53",
|
"@iconify-json/mdi": "^1.1.54",
|
||||||
"@types/echarts": "^4.9.18",
|
"@types/echarts": "^4.9.18",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^6.7.0",
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
"@vue/compiler-sfc": "3.3.4",
|
"@vue/compiler-sfc": "3.3.4",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/runtime-dom": "^3.3.4",
|
"@vue/runtime-dom": "^3.3.4",
|
||||||
"@vue/test-utils": "^2.4.0",
|
"@vue/test-utils": "^2.4.1",
|
||||||
"cypress": "^12.17.1",
|
"cypress": "^13.2.0",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"mutationobserver-shim": "^0.3.7",
|
"mutationobserver-shim": "^0.3.7",
|
||||||
"portal-vue": "^3.0.0",
|
"portal-vue": "^3.0.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.3",
|
||||||
"sass": "^1.63.6",
|
"sass": "^1.66.1",
|
||||||
"typescript": "~5.1.6",
|
"typescript": "~5.2.2",
|
||||||
"unplugin-icons": "^0.16.3",
|
"unplugin-icons": "^0.17.0",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"unplugin-vue-components": "^0.25.2",
|
||||||
"vite": "^4.4.3",
|
"vite": "^4.4.9",
|
||||||
"vitest": "^0.33.0",
|
"vitest": "^0.34.4",
|
||||||
"vue-tsc": "^1.8.4"
|
"vue-tsc": "^1.8.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,15 +24,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
import { BotDescriptor } from '@/types';
|
import { BotDescriptor } from '@/types';
|
||||||
import { ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bot: { type: Object as () => BotDescriptor, required: true },
|
bot: { type: Object as () => BotDescriptor, required: true },
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['cancelled', 'saved']);
|
const emit = defineEmits(['cancelled', 'saved']);
|
||||||
const botStore = useBotStore();
|
const botStore = useBotStore();
|
||||||
|
const newName = ref<string>('');
|
||||||
|
|
||||||
const newName = ref<string>(props.bot.botName);
|
onMounted(() => {
|
||||||
|
newName.value = props.bot.botName;
|
||||||
|
});
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
botStore.updateBot(props.bot.botId, {
|
botStore.updateBot(props.bot.botId, {
|
||||||
|
|
|
@ -1,31 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<e-charts
|
<e-charts
|
||||||
v-if="currencies"
|
v-if="currencies"
|
||||||
|
ref="balanceChart"
|
||||||
:option="balanceChartOptions"
|
:option="balanceChartOptions"
|
||||||
:theme="settingsStore.chartTheme"
|
:theme="settingsStore.chartTheme"
|
||||||
|
:style="{ height: width * 0.6 + 'px' }"
|
||||||
autoresize
|
autoresize
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ECharts from 'vue-echarts';
|
|
||||||
import { EChartsOption } from 'echarts';
|
import { EChartsOption } from 'echarts';
|
||||||
|
import ECharts from 'vue-echarts';
|
||||||
|
|
||||||
import { use } from 'echarts/core';
|
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
|
||||||
import { PieChart } from 'echarts/charts';
|
import { PieChart } from 'echarts/charts';
|
||||||
import { LabelLayout } from 'echarts/features';
|
|
||||||
import {
|
import {
|
||||||
DatasetComponent,
|
DatasetComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
import { use } from 'echarts/core';
|
||||||
|
import { LabelLayout } from 'echarts/features';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
|
||||||
import { BalanceValues } from '@/types';
|
|
||||||
import { formatPriceCurrency } from '@/shared/formatters';
|
import { formatPriceCurrency } from '@/shared/formatters';
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useSettingsStore } from '@/stores/settings';
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
import { BalanceValues } from '@/types';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
PieChart,
|
PieChart,
|
||||||
|
@ -37,6 +40,9 @@ use([
|
||||||
LabelLayout,
|
LabelLayout,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const balanceChart = ref(null);
|
||||||
|
const { width } = useElementSize(balanceChart);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
currencies: { required: true, type: Array as () => BalanceValues[] },
|
currencies: { required: true, type: Array as () => BalanceValues[] },
|
||||||
showTitle: { required: false, type: Boolean },
|
showTitle: { required: false, type: Boolean },
|
||||||
|
@ -93,8 +99,6 @@ const balanceChartOptions = computed((): EChartsOption => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.echarts {
|
.echarts {
|
||||||
width: 100%;
|
min-height: 20px;
|
||||||
height: 100%;
|
|
||||||
min-height: 240px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -37,6 +37,9 @@ import {
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
VisualMapComponent,
|
VisualMapComponent,
|
||||||
VisualMapPiecewiseComponent,
|
VisualMapPiecewiseComponent,
|
||||||
|
// MarkAreaComponent,
|
||||||
|
MarkLineComponent,
|
||||||
|
// MarkPointComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import { use } from 'echarts/core';
|
import { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
@ -54,6 +57,9 @@ use([
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
VisualMapComponent,
|
VisualMapComponent,
|
||||||
VisualMapPiecewiseComponent,
|
VisualMapPiecewiseComponent,
|
||||||
|
// MarkAreaComponent,
|
||||||
|
MarkLineComponent,
|
||||||
|
// MarkPointComponent,
|
||||||
|
|
||||||
CandlestickChart,
|
CandlestickChart,
|
||||||
BarChart,
|
BarChart,
|
||||||
|
@ -135,7 +141,8 @@ function updateChart(initial = false) {
|
||||||
if (chartOptions.value?.title) {
|
if (chartOptions.value?.title) {
|
||||||
chartOptions.value.title[0].text = chartTitle.value;
|
chartOptions.value.title[0].text = chartTitle.value;
|
||||||
}
|
}
|
||||||
const columns = props.dataset.columns;
|
// Avoid mutation of dataset.columns array
|
||||||
|
const columns = props.dataset.columns.slice();
|
||||||
|
|
||||||
const colDate = columns.findIndex((el) => el === '__date_ts');
|
const colDate = columns.findIndex((el) => el === '__date_ts');
|
||||||
const colOpen = columns.findIndex((el) => el === 'open');
|
const colOpen = columns.findIndex((el) => el === 'open');
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">
|
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">
|
||||||
<i-mdi-refresh />
|
<i-mdi-refresh />
|
||||||
</b-button>
|
</b-button>
|
||||||
|
<div class="d-flex flex-row flex-wrap">
|
||||||
<small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals"
|
<small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals"
|
||||||
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
|
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
|
||||||
>
|
>
|
||||||
|
@ -31,9 +32,10 @@
|
||||||
>Short exits: {{ dataset.exit_short_signals }}</small
|
>Short exits: {{ dataset.exit_short_signals }}</small
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="ms-auto d-flex align-items-center w-auto">
|
<div class="ms-auto d-flex align-items-center w-auto">
|
||||||
<b-form-checkbox v-model="settingsStore.useHeikinAshiCandles"
|
<b-form-checkbox v-model="settingsStore.useHeikinAshiCandles"
|
||||||
>Heikin Ashi</b-form-checkbox
|
><span class="text-nowrap">Heikin Ashi</span></b-form-checkbox
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
|
@ -121,7 +123,7 @@ const botStore = useBotStore();
|
||||||
const plotStore = usePlotConfigStore();
|
const plotStore = usePlotConfigStore();
|
||||||
|
|
||||||
const pair = ref('');
|
const pair = ref('');
|
||||||
const showPlotConfig = ref(props.plotConfigModal);
|
const showPlotConfig = ref<boolean>();
|
||||||
|
|
||||||
const dataset = computed((): PairHistory => {
|
const dataset = computed((): PairHistory => {
|
||||||
if (props.historicView) {
|
if (props.historicView) {
|
||||||
|
@ -159,14 +161,16 @@ const noDatasetText = computed((): string => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const showPlotConfigModal = ref(false);
|
const showPlotConfigModal = ref(false);
|
||||||
const showConfigurator = () => {
|
|
||||||
|
function showConfigurator() {
|
||||||
if (props.plotConfigModal) {
|
if (props.plotConfigModal) {
|
||||||
showPlotConfigModal.value = !showPlotConfigModal.value;
|
showPlotConfigModal.value = !showPlotConfigModal.value;
|
||||||
} else {
|
} else {
|
||||||
showPlotConfig.value = !showPlotConfig.value;
|
showPlotConfig.value = !showPlotConfig.value;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const refresh = () => {
|
|
||||||
|
function refresh() {
|
||||||
console.log('refresh', pair.value, props.timeframe);
|
console.log('refresh', pair.value, props.timeframe);
|
||||||
if (pair.value && props.timeframe) {
|
if (pair.value && props.timeframe) {
|
||||||
if (props.historicView) {
|
if (props.historicView) {
|
||||||
|
@ -181,11 +185,10 @@ const refresh = () => {
|
||||||
botStore.activeBot.getPairCandles({
|
botStore.activeBot.getPairCandles({
|
||||||
pair: pair.value,
|
pair: pair.value,
|
||||||
timeframe: props.timeframe,
|
timeframe: props.timeframe,
|
||||||
limit: 500,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.availablePairs,
|
() => props.availablePairs,
|
||||||
|
@ -206,6 +209,7 @@ watch(
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
showPlotConfig.value = props.plotConfigModal;
|
||||||
if (botStore.activeBot.selectedPair) {
|
if (botStore.activeBot.selectedPair) {
|
||||||
pair.value = botStore.activeBot.selectedPair;
|
pair.value = botStore.activeBot.selectedPair;
|
||||||
} else if (props.availablePairs.length > 0) {
|
} else if (props.availablePairs.length > 0) {
|
||||||
|
|
|
@ -56,7 +56,6 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
// const botList = ref<string[]>([]);
|
// const botList = ref<string[]>([]);
|
||||||
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
|
|
||||||
|
|
||||||
const chart = ref<typeof ECharts>();
|
const chart = ref<typeof ECharts>();
|
||||||
|
|
||||||
|
@ -107,20 +106,26 @@ const cumulativeData = computed<CumProfitChartData[]>(() => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (props.openTrades.length > 0 && valueArray.length > 0) {
|
if (props.openTrades.length > 0) {
|
||||||
|
let lastProfit = 0;
|
||||||
|
let lastDate = 0;
|
||||||
|
if (valueArray.length > 0) {
|
||||||
const lastPoint = valueArray[valueArray.length - 1];
|
const lastPoint = valueArray[valueArray.length - 1];
|
||||||
if (lastPoint) {
|
lastProfit = lastPoint.profit ?? 0;
|
||||||
const resultWitHOpen = (lastPoint.profit ?? 0) + openProfit.value;
|
lastDate = lastPoint.date ?? 0;
|
||||||
valueArray.push({ date: lastPoint.date, currentProfit: lastPoint.profit });
|
} else {
|
||||||
|
lastDate = props.openTrades[0].open_timestamp;
|
||||||
|
}
|
||||||
|
const resultWitHOpen = (lastProfit ?? 0) + openProfit.value;
|
||||||
|
valueArray.push({ date: lastDate, currentProfit: lastProfit });
|
||||||
// Add one day to date to ensure it's showing properly
|
// Add one day to date to ensure it's showing properly
|
||||||
const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
|
const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
|
||||||
valueArray.push({ date: tomorrow, currentProfit: resultWitHOpen });
|
valueArray.push({ date: tomorrow, currentProfit: resultWitHOpen });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return valueArray;
|
return valueArray;
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateChart(initial = false) {
|
function generateChart(initial = false) {
|
||||||
const { colorProfit, colorLoss } = settingsStore;
|
const { colorProfit, colorLoss } = settingsStore;
|
||||||
const chartOptionsLoc: EChartsOption = {
|
const chartOptionsLoc: EChartsOption = {
|
||||||
dataset: {
|
dataset: {
|
||||||
|
@ -167,7 +172,6 @@ function updateChart(initial = false) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: maybe have profit lines per bot?
|
// TODO: maybe have profit lines per bot?
|
||||||
// this.botList.forEach((botId: string) => {
|
// this.botList.forEach((botId: string) => {
|
||||||
// console.log('bot', botId);
|
// console.log('bot', botId);
|
||||||
|
@ -184,6 +188,10 @@ function updateChart(initial = false) {
|
||||||
// // symbol: 'none',
|
// // symbol: 'none',
|
||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
|
return chartOptionsLoc;
|
||||||
|
}
|
||||||
|
function updateChart(initial = false) {
|
||||||
|
const chartOptionsLoc = generateChart(initial);
|
||||||
chart.value?.setOption(chartOptionsLoc, {
|
chart.value?.setOption(chartOptionsLoc, {
|
||||||
replaceMerge: ['series', 'dataset'],
|
replaceMerge: ['series', 'dataset'],
|
||||||
noMerge: !initial,
|
noMerge: !initial,
|
||||||
|
@ -256,6 +264,11 @@ function initializeChart() {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
const chartOptionsLoc1 = generateChart(true);
|
||||||
|
// Merge the series and dataset, but not the rest
|
||||||
|
chartOptionsLoc.series = chartOptionsLoc1.series;
|
||||||
|
chartOptionsLoc.dataset = chartOptionsLoc1.dataset;
|
||||||
|
|
||||||
chart.value?.setOption(chartOptionsLoc, { noMerge: true });
|
chart.value?.setOption(chartOptionsLoc, { noMerge: true });
|
||||||
updateChart(true);
|
updateChart(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
v-model="fillTo"
|
v-model="fillTo"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
class="mt-1"
|
class="mt-1"
|
||||||
label="Select indicator to add"
|
label="Area chart - Fill to (leave empty for line chart)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import vSelect from 'vue-select';
|
import vSelect from 'vue-select';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -27,7 +27,7 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['update:modelValue', 'indicatorSelected']);
|
const emit = defineEmits(['update:modelValue', 'indicatorSelected']);
|
||||||
|
|
||||||
const selAvailableIndicator = ref(props.modelValue || '');
|
const selAvailableIndicator = ref('');
|
||||||
|
|
||||||
function emitIndicator() {
|
function emitIndicator() {
|
||||||
emit('indicatorSelected', selAvailableIndicator.value);
|
emit('indicatorSelected', selAvailableIndicator.value);
|
||||||
|
@ -37,6 +37,11 @@ function abort() {
|
||||||
selAvailableIndicator.value = '';
|
selAvailableIndicator.value = '';
|
||||||
emitIndicator();
|
emitIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
selAvailableIndicator.value = props.modelValue;
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<e-charts
|
<e-charts
|
||||||
v-if="dailyStats.data"
|
v-if="dailyStats.data"
|
||||||
|
ref="dailyChart"
|
||||||
:option="dailyChartOptions"
|
:option="dailyChartOptions"
|
||||||
:theme="settingsStore.chartTheme"
|
:theme="settingsStore.chartTheme"
|
||||||
|
:style="{ height: width * 0.6 + 'px' }"
|
||||||
autoresize
|
autoresize
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ComputedRef } from 'vue';
|
import { computed, ComputedRef, ref } from 'vue';
|
||||||
import ECharts from 'vue-echarts';
|
import ECharts from 'vue-echarts';
|
||||||
// import { EChartsOption } from 'echarts';
|
// import { EChartsOption } from 'echarts';
|
||||||
|
|
||||||
|
@ -24,9 +26,10 @@ import {
|
||||||
VisualMapComponent,
|
VisualMapComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
|
||||||
import { DailyReturnValue } from '@/types';
|
import { TimeSummaryReturnValue } from '@/types';
|
||||||
import { useSettingsStore } from '@/stores/settings';
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
import { EChartsOption } from 'echarts';
|
import { EChartsOption } from 'echarts';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
BarChart,
|
BarChart,
|
||||||
|
@ -46,7 +49,7 @@ const CHART_TRADE_COUNT = 'Trade Count';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dailyStats: {
|
dailyStats: {
|
||||||
type: Object as () => DailyReturnValue,
|
type: Object as () => TimeSummaryReturnValue,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
showTitle: {
|
showTitle: {
|
||||||
|
@ -56,6 +59,10 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
|
const dailyChart = ref(null);
|
||||||
|
const { width } = useElementSize(dailyChart);
|
||||||
|
|
||||||
const absoluteMin = computed(() =>
|
const absoluteMin = computed(() =>
|
||||||
props.dailyStats.data.reduce(
|
props.dailyStats.data.reduce(
|
||||||
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
||||||
|
@ -125,7 +132,7 @@ const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
|
||||||
},
|
},
|
||||||
nameRotate: 90,
|
nameRotate: 90,
|
||||||
nameLocation: 'middle',
|
nameLocation: 'middle',
|
||||||
nameGap: 40,
|
nameGap: 35,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
@ -156,8 +163,6 @@ const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.echarts {
|
.echarts {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 240px;
|
min-height: 240px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -12,32 +12,64 @@
|
||||||
Load Historic results from disk. You can click on multiple results to load all of them into
|
Load Historic results from disk. You can click on multiple results to load all of them into
|
||||||
freqUI.
|
freqUI.
|
||||||
</p>
|
</p>
|
||||||
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ms-2">
|
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ms-2 mb-1">
|
||||||
<b-list-group-item
|
<b-list-group-item
|
||||||
v-for="(res, idx) in botStore.activeBot.backtestHistoryList"
|
v-for="(res, idx) in botStore.activeBot.backtestHistoryList"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="d-flex justify-content-between align-items-center py-1 mb-1"
|
class="d-flex justify-content-between align-items-center py-1 pe-2"
|
||||||
button
|
button
|
||||||
|
:disabled="res.run_id in botStore.activeBot.backtestHistory"
|
||||||
@click="botStore.activeBot.getBacktestHistoryResult(res)"
|
@click="botStore.activeBot.getBacktestHistoryResult(res)"
|
||||||
>
|
>
|
||||||
<strong>{{ res.strategy }}</strong>
|
<strong>{{ res.strategy }}</strong>
|
||||||
backtested on: {{ timestampms(res.backtest_start_time * 1000) }}
|
backtested on: {{ timestampms(res.backtest_start_time * 1000) }}
|
||||||
<small>{{ res.filename }}</small>
|
<small>{{ res.filename }}</small>
|
||||||
|
<InfoBox
|
||||||
|
v-if="botStore.activeBot.botApiVersion >= 2.32"
|
||||||
|
:class="res.notes ? 'opacity-100' : 'opacity-0'"
|
||||||
|
:hint="res.notes ?? ''"
|
||||||
|
></InfoBox>
|
||||||
|
<b-button
|
||||||
|
v-if="botStore.activeBot.botApiVersion >= 2.31"
|
||||||
|
class="ms-1"
|
||||||
|
size="sm"
|
||||||
|
title="Delete this Result."
|
||||||
|
:disabled="res.run_id in botStore.activeBot.backtestHistory"
|
||||||
|
@click.stop="deleteBacktestResult(res)"
|
||||||
|
>
|
||||||
|
<i-mdi-delete />
|
||||||
|
</b-button>
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
</div>
|
</div>
|
||||||
|
<MessageBox ref="msgBox" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import MessageBox, { MsgBoxObject } from '@/components/general/MessageBox.vue';
|
||||||
import { timestampms } from '@/shared/formatters';
|
import { timestampms } from '@/shared/formatters';
|
||||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
|
import { BacktestHistoryEntry } from '@/types';
|
||||||
|
import InfoBox from '../general/InfoBox.vue';
|
||||||
|
|
||||||
const botStore = useBotStore();
|
const botStore = useBotStore();
|
||||||
|
const msgBox = ref<typeof MessageBox>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
botStore.activeBot.getBacktestHistory();
|
botStore.activeBot.getBacktestHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function deleteBacktestResult(result: BacktestHistoryEntry) {
|
||||||
|
const msg: MsgBoxObject = {
|
||||||
|
title: 'Stop Bot',
|
||||||
|
message: `Delete result ${result.filename} from disk?`,
|
||||||
|
accept: () => {
|
||||||
|
botStore.activeBot.deleteBacktestHistoryResult(result);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
msgBox.value?.show(msg);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container-fluid px-0 backtestresult-container">
|
<div class="px-0 mw-100">
|
||||||
<div class="row d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<h3>Backtest-result for {{ backtestResult.strategy_name }}</h3>
|
<h3>Backtest-result for {{ backtestResult.strategy_name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row text-start ms-0">
|
<div class="d-flex flex-column text-start ms-0 me-2 gap-2">
|
||||||
<div class="row w-100">
|
<div class="d-flex flex-column flex-xl-row">
|
||||||
<div class="col-12 col-xl-6 px-0 px-xl-0 pe-xl-1">
|
<div class="px-0 px-xl-0 pe-xl-1 flex-fill">
|
||||||
<b-card header="Strategy settings">
|
<b-card header="Strategy settings">
|
||||||
<b-table
|
<b-table
|
||||||
small
|
small
|
||||||
|
@ -17,14 +17,14 @@
|
||||||
</b-table>
|
</b-table>
|
||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-6 px-0 px-xl-0 pt-2 pt-xl-0 ps-xl-1">
|
<div class="px-0 px-xl-0 pt-2 pt-xl-0 ps-xl-1 flex-fill">
|
||||||
<b-card header="Metrics">
|
<b-card header="Metrics">
|
||||||
<b-table small borderless :items="backtestResultStats" :fields="backtestResultFields">
|
<b-table small borderless :items="backtestResultStats" :fields="backtestResultFields">
|
||||||
</b-table>
|
</b-table>
|
||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<b-card header="Results per Exit-reason" class="row mt-2 w-100">
|
<b-card header="Results per Exit-reason">
|
||||||
<b-table
|
<b-table
|
||||||
small
|
small
|
||||||
hover
|
hover
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
>
|
>
|
||||||
</b-table>
|
</b-table>
|
||||||
</b-card>
|
</b-card>
|
||||||
<b-card header="Results per pair" class="row mt-2 w-100">
|
<b-card header="Results per pair">
|
||||||
<b-table
|
<b-table
|
||||||
small
|
small
|
||||||
hover
|
hover
|
||||||
|
@ -47,18 +47,13 @@
|
||||||
>
|
>
|
||||||
</b-table>
|
</b-table>
|
||||||
</b-card>
|
</b-card>
|
||||||
<b-card
|
<b-card v-if="backtestResult.periodic_breakdown" header="Periodic breakdown">
|
||||||
v-if="backtestResult.periodic_breakdown"
|
|
||||||
header="Periodic breakdown"
|
|
||||||
class="row mt-2 w-100"
|
|
||||||
>
|
|
||||||
<BacktestResultPeriodBreakdown :periodic-breakdown="backtestResult.periodic_breakdown">
|
<BacktestResultPeriodBreakdown :periodic-breakdown="backtestResult.periodic_breakdown">
|
||||||
</BacktestResultPeriodBreakdown>
|
</BacktestResultPeriodBreakdown>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
|
||||||
<b-card header="Single trades" class="row mt-2 w-100">
|
<b-card header="Single trades">
|
||||||
<TradeList
|
<TradeList
|
||||||
class="row trade-history mt-2 w-100"
|
|
||||||
:trades="backtestResult.trades"
|
:trades="backtestResult.trades"
|
||||||
:show-filter="true"
|
:show-filter="true"
|
||||||
:stake-currency="backtestResult.stake_currency"
|
:stake-currency="backtestResult.stake_currency"
|
||||||
|
@ -79,6 +74,7 @@ import {
|
||||||
formatPercent,
|
formatPercent,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
humanizeDurationFromSeconds,
|
humanizeDurationFromSeconds,
|
||||||
|
isNotUndefined,
|
||||||
} from '@/shared/formatters';
|
} from '@/shared/formatters';
|
||||||
import { TableField, TableItem } from 'bootstrap-vue-next';
|
import { TableField, TableItem } from 'bootstrap-vue-next';
|
||||||
|
|
||||||
|
@ -115,6 +111,10 @@ const worstPair = computed((): string => {
|
||||||
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
|
return `${value.pair} ${formatPercent(value.profit_ratio, 2)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pairSummary = computed(() => {
|
||||||
|
return props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1];
|
||||||
|
});
|
||||||
|
|
||||||
const backtestResultStats = computed(() => {
|
const backtestResultStats = computed(() => {
|
||||||
// Transpose Result into readable format
|
// Transpose Result into readable format
|
||||||
const shortMetrics =
|
const shortMetrics =
|
||||||
|
@ -164,9 +164,16 @@ const backtestResultStats = computed(() => {
|
||||||
value: `${props.backtestResult.calmar ? props.backtestResult.calmar.toFixed(2) : 'N/A'}`,
|
value: `${props.backtestResult.calmar ? props.backtestResult.calmar.toFixed(2) : 'N/A'}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
metric: 'Expectancy',
|
metric: `Expectancy ${props.backtestResult.expectancy_ratio ? '(ratio)' : ''}`,
|
||||||
value: `${
|
value: `${
|
||||||
props.backtestResult.expectancy ? props.backtestResult.expectancy.toFixed(2) : 'N/A'
|
props.backtestResult.expectancy
|
||||||
|
? props.backtestResult.expectancy_ratio
|
||||||
|
? props.backtestResult.expectancy.toFixed(2) +
|
||||||
|
' (' +
|
||||||
|
props.backtestResult.expectancy_ratio.toFixed(2) +
|
||||||
|
')'
|
||||||
|
: props.backtestResult.expectancy.toFixed(2)
|
||||||
|
: 'N/A'
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -198,14 +205,19 @@ const backtestResultStats = computed(() => {
|
||||||
|
|
||||||
{
|
{
|
||||||
metric: 'Win/Draw/Loss',
|
metric: 'Win/Draw/Loss',
|
||||||
value: `${
|
value: `${pairSummary.value.wins} / ${pairSummary.value.draws} / ${
|
||||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1].wins
|
pairSummary.value.losses
|
||||||
} / ${
|
} ${
|
||||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
isNotUndefined(pairSummary.value.winrate)
|
||||||
.draws
|
? '(WR: ' +
|
||||||
} / ${
|
formatPercent(
|
||||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
props.backtestResult.results_per_pair[
|
||||||
.losses
|
props.backtestResult.results_per_pair.length - 1
|
||||||
|
].winrate ?? 0,
|
||||||
|
2,
|
||||||
|
) +
|
||||||
|
')'
|
||||||
|
: ''
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -221,6 +233,13 @@ const backtestResultStats = computed(() => {
|
||||||
metric: 'Avg. Duration Losers',
|
metric: 'Avg. Duration Losers',
|
||||||
value: humanizeDurationFromSeconds(props.backtestResult.loser_holding_avg_s),
|
value: humanizeDurationFromSeconds(props.backtestResult.loser_holding_avg_s),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
metric: 'Max Consecutive Wins / Loss',
|
||||||
|
value:
|
||||||
|
props.backtestResult.max_consecutive_wins === undefined
|
||||||
|
? 'N/A'
|
||||||
|
: `${props.backtestResult.max_consecutive_wins} / ${props.backtestResult.max_consecutive_losses}`,
|
||||||
|
},
|
||||||
{ metric: 'Rejected entry signals', value: props.backtestResult.rejected_signals },
|
{ metric: 'Rejected entry signals', value: props.backtestResult.rejected_signals },
|
||||||
{
|
{
|
||||||
metric: 'Entry/Exit timeouts',
|
metric: 'Entry/Exit timeouts',
|
||||||
|
@ -413,10 +432,4 @@ const backtestsettingFields: TableField[] = [
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
.backtestresult-container {
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
max-width: 1200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -15,7 +15,7 @@ const periodicBreakdownSelections = [
|
||||||
{ value: 'month', text: 'Months' },
|
{ value: 'month', text: 'Months' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const periodicBreakdownPeriod = ref<string>('day');
|
const periodicBreakdownPeriod = ref<string>('month');
|
||||||
|
|
||||||
const periodicBreakdownFields = computed<TableField[]>(() => {
|
const periodicBreakdownFields = computed<TableField[]>(() => {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -1,16 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container d-flex flex-column align-items-center">
|
<div class="container d-flex flex-column align-items-stretch">
|
||||||
<h3>Available results:</h3>
|
<h3>Available results:</h3>
|
||||||
<b-list-group class="ms-2">
|
<b-list-group class="ms-2">
|
||||||
<b-list-group-item
|
<b-list-group-item
|
||||||
v-for="[key, strat] in Object.entries(backtestHistory)"
|
v-for="[key, result] in Object.entries(backtestHistory)"
|
||||||
:key="key"
|
:key="key"
|
||||||
button
|
button
|
||||||
:active="key === selectedBacktestResultKey"
|
:active="key === selectedBacktestResultKey"
|
||||||
class="d-flex justify-content-between align-items-center py-1"
|
class="d-flex justify-content-between align-items-center py-1 pe-1"
|
||||||
@click="setBacktestResult(key)"
|
@click="setBacktestResult(key)"
|
||||||
>
|
>
|
||||||
{{ key }} {{ strat.total_trades }} {{ formatPercent(strat.profit_total) }}
|
<template v-if="!result.metadata.editing">
|
||||||
|
<div class="d-flex flex-column me-2 text-start">
|
||||||
|
<div class="fw-bold">
|
||||||
|
{{ result.metadata.strategyName }} - {{ result.strategy.timeframe }}
|
||||||
|
</div>
|
||||||
|
<div class="text-small">
|
||||||
|
TradeCount: {{ result.strategy.total_trades }} - Profit:
|
||||||
|
{{ formatPercent(result.strategy.profit_total) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="canUseModify" class="text-small" style="white-space: pre-wrap">
|
||||||
|
{{ result.metadata.notes }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<b-button
|
||||||
|
v-if="canUseModify"
|
||||||
|
class="flex-nowrap"
|
||||||
|
size="sm"
|
||||||
|
title="Modify"
|
||||||
|
@click.stop="result.metadata.editing = !result.metadata.editing"
|
||||||
|
>
|
||||||
|
<i-mdi-pencil />
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
size="sm"
|
||||||
|
class="flex-nowrap"
|
||||||
|
title="Delete this Result."
|
||||||
|
@click.stop="emit('removeResult', key)"
|
||||||
|
>
|
||||||
|
<i-mdi-delete />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="result.metadata.editing">
|
||||||
|
<b-form-textarea v-model="result.metadata.notes" placeholder="notes" size="sm">
|
||||||
|
</b-form-textarea>
|
||||||
|
|
||||||
|
<b-button size="sm" title="Confirm" @click.stop="confirmInput(key, result)">
|
||||||
|
<i-mdi-check />
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,19 +58,37 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatPercent } from '@/shared/formatters';
|
import { formatPercent } from '@/shared/formatters';
|
||||||
import { StrategyBacktestResult } from '@/types';
|
import { BacktestResultInMemory, BacktestResultUpdate } from '@/types';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
backtestHistory: {
|
backtestHistory: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object as () => Record<string, StrategyBacktestResult>,
|
type: Object as () => Record<string, BacktestResultInMemory>,
|
||||||
},
|
},
|
||||||
selectedBacktestResultKey: { required: false, default: '', type: String },
|
selectedBacktestResultKey: { required: false, default: '', type: String },
|
||||||
|
canUseModify: { required: false, default: false, type: Boolean },
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['selectionChange']);
|
const emit = defineEmits<{
|
||||||
const setBacktestResult = (key) => {
|
selectionChange: [value: string];
|
||||||
|
removeResult: [value: string];
|
||||||
|
updateResult: [value: BacktestResultUpdate];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const setBacktestResult = (key: string) => {
|
||||||
emit('selectionChange', key);
|
emit('selectionChange', key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function confirmInput(run_id: string, result: BacktestResultInMemory) {
|
||||||
|
result.metadata.editing = !result.metadata.editing;
|
||||||
|
if (result.metadata.filename) {
|
||||||
|
emit('updateResult', {
|
||||||
|
run_id: run_id,
|
||||||
|
notes: result.metadata.notes ?? '',
|
||||||
|
filename: result.metadata.filename,
|
||||||
|
strategy: result.metadata.strategyName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
258
src/components/ftbot/BacktestRun.vue
Normal file
258
src/components/ftbot/BacktestRun.vue
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span>Strategy</span>
|
||||||
|
<StrategySelect v-model="btStore.strategy"></StrategySelect>
|
||||||
|
</div>
|
||||||
|
<b-card :disabled="botStore.activeBot.backtestRunning">
|
||||||
|
<!-- Backtesting parameters -->
|
||||||
|
<b-form-group
|
||||||
|
label-cols-lg="2"
|
||||||
|
label="Backtest params"
|
||||||
|
label-size="sm"
|
||||||
|
label-class="fw-bold pt-0"
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Timeframe:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="timeframe-select"
|
||||||
|
>
|
||||||
|
<TimeframeSelect id="timeframe-select" v-model="btStore.selectedTimeframe" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Detail Timeframe:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="timeframe-detail-select"
|
||||||
|
title="Detail timeframe, to simulate intra-candle results. Not setting this will not use this functionality."
|
||||||
|
>
|
||||||
|
<TimeframeSelect
|
||||||
|
id="timeframe-detail-select"
|
||||||
|
v-model="btStore.selectedDetailTimeframe"
|
||||||
|
:below-timeframe="btStore.selectedTimeframe"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Max open trades:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="max-open-trades"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="max-open-trades"
|
||||||
|
v-model="btStore.maxOpenTrades"
|
||||||
|
placeholder="Use strategy default"
|
||||||
|
type="number"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Starting capital:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="starting-capital"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="starting-capital"
|
||||||
|
v-model="btStore.startingCapital"
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Stake amount:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="stake-amount"
|
||||||
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<b-form-checkbox
|
||||||
|
id="stake-amount-bool"
|
||||||
|
v-model="btStore.stakeAmountUnlimited"
|
||||||
|
class="col-md-6"
|
||||||
|
>Unlimited stake</b-form-checkbox
|
||||||
|
>
|
||||||
|
|
||||||
|
<b-form-input
|
||||||
|
id="stake-amount"
|
||||||
|
v-model="btStore.stakeAmount"
|
||||||
|
type="number"
|
||||||
|
placeholder="Use strategy default"
|
||||||
|
step="0.01"
|
||||||
|
:disabled="btStore.stakeAmountUnlimited"
|
||||||
|
></b-form-input>
|
||||||
|
</div>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Enable Protections:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="enable-protections"
|
||||||
|
>
|
||||||
|
<b-form-checkbox
|
||||||
|
id="enable-protections"
|
||||||
|
v-model="btStore.enableProtections"
|
||||||
|
></b-form-checkbox>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
v-if="botStore.activeBot.botApiVersion >= 2.22"
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Cache Backtest results:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="enable-cache"
|
||||||
|
>
|
||||||
|
<b-form-checkbox id="enable-cache" v-model="btStore.allowCache"></b-form-checkbox>
|
||||||
|
</b-form-group>
|
||||||
|
<template v-if="botStore.activeBot.botApiVersion >= 2.22">
|
||||||
|
<b-form-group
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="Enable FreqAI:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="enable-freqai"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<span class="me-2">Enable FreqAI:</span>
|
||||||
|
<InfoBox
|
||||||
|
hint="Assumes freqAI configuration is setup in the configuration, and the strategy is a freqAI strategy. Will fail if that's not the case."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<b-form-checkbox id="enable-freqai" v-model="btStore.freqAI.enabled"></b-form-checkbox>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
v-if="btStore.freqAI.enabled"
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="FreqAI identifier:"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="freqai-identifier"
|
||||||
|
>
|
||||||
|
<b-form-input
|
||||||
|
id="freqai-identifier"
|
||||||
|
v-model="btStore.freqAI.identifier"
|
||||||
|
placeholder="Use config default"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group
|
||||||
|
v-if="btStore.freqAI.enabled"
|
||||||
|
label-cols-sm="5"
|
||||||
|
label="FreqAI Model"
|
||||||
|
label-align-sm="right"
|
||||||
|
label-for="freqai-model"
|
||||||
|
>
|
||||||
|
<FreqaiModelSelect id="freqai-model" v-model="btStore.freqAI.model"></FreqaiModelSelect>
|
||||||
|
</b-form-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <b-form-group label-cols-sm="5" label="Fee:" label-align-sm="right" label-for="fee">
|
||||||
|
<b-form-input
|
||||||
|
id="fee"
|
||||||
|
type="number"
|
||||||
|
placeholder="Use exchange default"
|
||||||
|
step="0.01"
|
||||||
|
></b-form-input>
|
||||||
|
</b-form-group> -->
|
||||||
|
<hr />
|
||||||
|
<TimeRangeSelect v-model="btStore.timerange" class="mt-2"></TimeRangeSelect>
|
||||||
|
</b-form-group>
|
||||||
|
</b-card>
|
||||||
|
|
||||||
|
<h3 class="mt-3">Backtesting summary</h3>
|
||||||
|
<div class="d-flex flex-wrap flex-md-nowrap justify-content-between justify-content-md-center">
|
||||||
|
<b-button
|
||||||
|
id="start-backtest"
|
||||||
|
variant="primary"
|
||||||
|
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
||||||
|
class="mx-1"
|
||||||
|
@click="clickBacktest"
|
||||||
|
>
|
||||||
|
Start backtest
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
variant="primary"
|
||||||
|
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
||||||
|
class="mx-1"
|
||||||
|
@click="botStore.activeBot.pollBacktest"
|
||||||
|
>
|
||||||
|
Load backtest result
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
variant="primary"
|
||||||
|
class="mx-1"
|
||||||
|
:disabled="!botStore.activeBot.backtestRunning"
|
||||||
|
@click="botStore.activeBot.stopBacktest"
|
||||||
|
>Stop Backtest</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
variant="primary"
|
||||||
|
class="mx-1"
|
||||||
|
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
||||||
|
@click="botStore.activeBot.removeBacktest"
|
||||||
|
>Reset Backtest</b-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TimeRangeSelect from '@/components/ftbot/TimeRangeSelect.vue';
|
||||||
|
|
||||||
|
import FreqaiModelSelect from '@/components/ftbot/FreqaiModelSelect.vue';
|
||||||
|
import StrategySelect from '@/components/ftbot/StrategySelect.vue';
|
||||||
|
import TimeframeSelect from '@/components/ftbot/TimeframeSelect.vue';
|
||||||
|
|
||||||
|
import InfoBox from '@/components/general/InfoBox.vue';
|
||||||
|
|
||||||
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
|
import { BacktestPayload } from '@/types';
|
||||||
|
|
||||||
|
import { useBtStore } from '@/stores/btStore';
|
||||||
|
const botStore = useBotStore();
|
||||||
|
const btStore = useBtStore();
|
||||||
|
|
||||||
|
function clickBacktest() {
|
||||||
|
const btPayload: BacktestPayload = {
|
||||||
|
strategy: btStore.strategy,
|
||||||
|
timerange: btStore.timerange,
|
||||||
|
enable_protections: btStore.enableProtections,
|
||||||
|
};
|
||||||
|
const openTradesInt = parseInt(btStore.maxOpenTrades, 10);
|
||||||
|
if (openTradesInt) {
|
||||||
|
btPayload.max_open_trades = openTradesInt;
|
||||||
|
}
|
||||||
|
if (btStore.stakeAmountUnlimited) {
|
||||||
|
btPayload.stake_amount = 'unlimited';
|
||||||
|
} else {
|
||||||
|
const stakeAmountLoc = Number(btStore.stakeAmount);
|
||||||
|
if (stakeAmountLoc) {
|
||||||
|
btPayload.stake_amount = stakeAmountLoc.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingCapitalLoc = Number(btStore.startingCapital);
|
||||||
|
if (startingCapitalLoc) {
|
||||||
|
btPayload.dry_run_wallet = startingCapitalLoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btStore.selectedTimeframe) {
|
||||||
|
btPayload.timeframe = btStore.selectedTimeframe;
|
||||||
|
}
|
||||||
|
if (btStore.selectedDetailTimeframe) {
|
||||||
|
btPayload.timeframe_detail = btStore.selectedDetailTimeframe;
|
||||||
|
}
|
||||||
|
if (!btStore.allowCache) {
|
||||||
|
btPayload.backtest_cache = 'none';
|
||||||
|
}
|
||||||
|
if (btStore.freqAI.enabled) {
|
||||||
|
btPayload.freqaimodel = btStore.freqAI.model;
|
||||||
|
if (btStore.freqAI.identifier !== '') {
|
||||||
|
btPayload.freqai = { identifier: btStore.freqAI.identifier };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
botStore.activeBot.startBacktest(btPayload);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -33,23 +33,26 @@
|
||||||
</p>
|
</p>
|
||||||
<b-table class="table-sm" :items="balanceCurrencies" :fields="tableFields">
|
<b-table class="table-sm" :items="balanceCurrencies" :fields="tableFields">
|
||||||
<template #custom-foot>
|
<template #custom-foot>
|
||||||
<td><strong>Total</strong></td>
|
<td class="pt-1"><strong>Total</strong></td>
|
||||||
<td>
|
<td class="pt-1">
|
||||||
<span
|
<span
|
||||||
class="font-italic"
|
class="font-italic"
|
||||||
:title="`Increase over initial capital of ${formatCurrency(
|
:title="`Increase over initial capital of ${formatCurrency(
|
||||||
botStore.activeBot.balance.starting_capital,
|
botStore.activeBot.balance.starting_capital,
|
||||||
)} ${botStore.activeBot.balance.stake}`"
|
)} ${botStore.activeBot.balance.stake}`"
|
||||||
>{{ formatPercent(botStore.activeBot.balance.starting_capital_ratio) }}</span
|
|
||||||
>
|
>
|
||||||
|
{{ formatPercent(botStore.activeBot.balance.starting_capital_ratio) }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- this is a computed prop that adds up all the expenses in the visible rows -->
|
<!-- this is a computed prop that adds up all the expenses in the visible rows -->
|
||||||
<td>
|
<td class="pt-1">
|
||||||
<strong>{{
|
<strong>
|
||||||
|
{{
|
||||||
showBotOnly && canUseBotBalance
|
showBotOnly && canUseBotBalance
|
||||||
? formatCurrency(botStore.activeBot.balance.total_bot)
|
? formatCurrency(botStore.activeBot.balance.total_bot)
|
||||||
: formatCurrency(botStore.activeBot.balance.total)
|
: formatCurrency(botStore.activeBot.balance.total)
|
||||||
}}</strong>
|
}}
|
||||||
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</b-table>
|
||||||
|
|
|
@ -12,41 +12,50 @@
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<b-form-checkbox
|
<b-form-checkbox
|
||||||
v-if="row.item.botId && botStore.botCount > 1"
|
v-if="row.item.botId && botStore.botCount > 1"
|
||||||
v-model="botStore.botStores[row.item.botId].isSelected"
|
v-model="
|
||||||
|
botStore.botStores[(row.item as unknown as ComparisonTableItems).botId ?? ''].isSelected
|
||||||
|
"
|
||||||
title="Show bot in Dashboard"
|
title="Show bot in Dashboard"
|
||||||
/>
|
/>
|
||||||
<span>{{ row.value }}</span>
|
<span>{{ row.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(profitOpen)="row">
|
<template #cell(profitOpen)="{ item }">
|
||||||
<profit-pill
|
<profit-pill
|
||||||
v-if="row.item.profitOpen && row.item.botId != 'Summary'"
|
v-if="item.profitOpen && item.botId != 'Summary'"
|
||||||
:profit-ratio="row.item.profitOpenRatio"
|
:profit-ratio="(item as unknown as ComparisonTableItems).profitOpenRatio"
|
||||||
:profit-abs="row.item.profitOpen"
|
:profit-abs="(item as unknown as ComparisonTableItems).profitOpen"
|
||||||
:stake-currency="row.item.stakeCurrency"
|
:stake-currency="(item as unknown as ComparisonTableItems).stakeCurrency"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(profitClosed)="row">
|
<template #cell(profitClosed)="{ item }">
|
||||||
<profit-pill
|
<profit-pill
|
||||||
v-if="row.item.profitClosed && row.item.botId != 'Summary'"
|
v-if="item.profitClosed && item.botId != 'Summary'"
|
||||||
:profit-ratio="row.item.profitClosedRatio"
|
:profit-ratio="(item as unknown as ComparisonTableItems).profitClosedRatio"
|
||||||
:profit-abs="row.item.profitClosed"
|
:profit-abs="(item as unknown as ComparisonTableItems).profitClosed"
|
||||||
:stake-currency="row.item.stakeCurrency"
|
:stake-currency="(item as unknown as ComparisonTableItems).stakeCurrency"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell(balance)="row">
|
<template #cell(balance)="{ item }">
|
||||||
<div v-if="row.item.balance">
|
<div v-if="item.balance">
|
||||||
<span :title="row.item.stakeCurrency"
|
<span :title="(item as unknown as ComparisonTableItems).stakeCurrency"
|
||||||
>{{ formatPrice(row.item.balance, row.item.stakeCurrencyDecimals) }}
|
>{{
|
||||||
|
formatPrice(
|
||||||
|
(item as unknown as ComparisonTableItems).balance ?? 0,
|
||||||
|
(item as unknown as ComparisonTableItems).stakeCurrencyDecimals,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-small">{{ row.item.stakeCurrency }}</span>
|
<span class="text-small">{{
|
||||||
|
` ${item.stakeCurrency}${item.isDryRun ? ' (dry)' : ''}`
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(winVsLoss)="row">
|
<template #cell(winVsLoss)="{ item }">
|
||||||
<div v-if="row.item.losses !== undefined">
|
<div v-if="item.losses !== undefined">
|
||||||
<span class="text-profit">{{ row.item.wins }}</span> /
|
<span class="text-profit">{{ item.wins }}</span> /
|
||||||
<span class="text-loss">{{ row.item.losses }}</span>
|
<span class="text-loss">{{ item.losses }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</b-table>
|
||||||
|
@ -113,13 +122,20 @@ const tableItems = computed<TableItem[]>(() => {
|
||||||
losses: v.losing_trades,
|
losses: v.losing_trades,
|
||||||
balance: botStore.allBalance[k]?.total_bot ?? botStore.allBalance[k]?.total,
|
balance: botStore.allBalance[k]?.total_bot ?? botStore.allBalance[k]?.total,
|
||||||
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
|
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
|
||||||
|
isDryRun: botStore.allBotState[k]?.dry_run,
|
||||||
});
|
});
|
||||||
if (v.profit_closed_coin !== undefined) {
|
if (v.profit_closed_coin !== undefined) {
|
||||||
|
if (botStore.botStores[k].isSelected) {
|
||||||
|
// Summary should only include selected bots
|
||||||
summary.profitClosed += v.profit_closed_coin;
|
summary.profitClosed += v.profit_closed_coin;
|
||||||
summary.profitOpen += profitOpen;
|
summary.profitOpen += profitOpen;
|
||||||
summary.wins += v.winning_trades;
|
summary.wins += v.winning_trades;
|
||||||
summary.losses += v.losing_trades;
|
summary.losses += v.losing_trades;
|
||||||
// summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
|
// summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
|
||||||
|
// This will always take the last bot's stake currency
|
||||||
|
// And therefore may result in wrong values.
|
||||||
|
summary.stakeCurrency = botStore.allBotState[k]?.stake_currency || summary.stakeCurrency;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
val.push(summary);
|
val.push(summary);
|
||||||
|
|
131
src/components/ftbot/BotProfit.vue
Normal file
131
src/components/ftbot/BotProfit.vue
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
<template>
|
||||||
|
<b-table class="text-start" small borderless :items="profitItems" :fields="profitFields">
|
||||||
|
<template #cell(value)="row">
|
||||||
|
<DateTimeTZ v-if="row.item.isTs && row.value" :date="row.value as number"></DateTimeTZ>
|
||||||
|
<template v-else>{{ row.value }}</template>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatPercent, formatPriceCurrency, timestampms } from '@/shared/formatters';
|
||||||
|
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||||
|
|
||||||
|
import { ProfitInterface } from '@/types';
|
||||||
|
import { TableField, TableItem } from 'bootstrap-vue-next';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
profit: { required: true, type: Object as () => ProfitInterface },
|
||||||
|
stakeCurrency: { required: true, type: String },
|
||||||
|
stakeCurrencyDecimals: { required: true, type: Number },
|
||||||
|
});
|
||||||
|
|
||||||
|
const profitFields: TableField[] = [
|
||||||
|
{ key: 'metric', label: 'Metric' },
|
||||||
|
{ key: 'value', label: 'Value' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const profitItems = computed<TableItem[]>(() => {
|
||||||
|
if (!props.profit) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metric: 'ROI open trades',
|
||||||
|
value: props.profit.profit_closed_coin
|
||||||
|
? `${formatPriceCurrency(
|
||||||
|
props.profit.profit_closed_coin,
|
||||||
|
props.stakeCurrency,
|
||||||
|
props.stakeCurrencyDecimals,
|
||||||
|
)} (${formatPercent(props.profit.profit_closed_ratio_mean, 2)})`
|
||||||
|
: 'N/A',
|
||||||
|
// (∑ ${formatPercent(props.profit.profit_closed_ratio_sum, 2,)})`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'ROI all trades',
|
||||||
|
value: props.profit.profit_all_coin
|
||||||
|
? `${formatPriceCurrency(
|
||||||
|
props.profit.profit_all_coin,
|
||||||
|
props.stakeCurrency,
|
||||||
|
props.stakeCurrencyDecimals,
|
||||||
|
)} (${formatPercent(props.profit.profit_all_ratio_mean, 2)})`
|
||||||
|
: 'N/A',
|
||||||
|
// (∑ ${formatPercent(props.profit.profit_all_ratio_sum,2,)})`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
metric: 'Total Trade count',
|
||||||
|
value: `${props.profit.trade_count ?? 0}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Bot started',
|
||||||
|
value: props.profit.bot_start_timestamp,
|
||||||
|
isTs: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'First Trade opened',
|
||||||
|
value: props.profit.first_trade_timestamp,
|
||||||
|
isTs: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Latest Trade opened',
|
||||||
|
value: props.profit.latest_trade_timestamp,
|
||||||
|
isTs: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Win / Loss',
|
||||||
|
value: `${props.profit.winning_trades ?? 0} / ${props.profit.losing_trades ?? 0}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Winrate',
|
||||||
|
value: `${props.profit.winrate ? formatPercent(props.profit.winrate) : 'N/A'}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Expectancy (ratio)',
|
||||||
|
value: `${props.profit.expectancy ? props.profit.expectancy.toFixed(2) : 'N/A'} (${
|
||||||
|
props.profit.expectancy_ratio ? props.profit.expectancy_ratio.toFixed(2) : 'N/A'
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Avg. Duration',
|
||||||
|
value: `${props.profit.avg_duration ?? 'N/A'}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Best performing',
|
||||||
|
value: props.profit.best_pair
|
||||||
|
? `${props.profit.best_pair}: ${formatPercent(props.profit.best_pair_profit_ratio, 2)}`
|
||||||
|
: 'N/A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Trading volume',
|
||||||
|
value: `${formatPriceCurrency(
|
||||||
|
props.profit.trading_volume ?? 0,
|
||||||
|
props.stakeCurrency,
|
||||||
|
props.stakeCurrencyDecimals,
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Profit factor',
|
||||||
|
value: `${props.profit.profit_factor ? props.profit.profit_factor.toFixed(2) : 'N/A'}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: 'Max Drawdown',
|
||||||
|
value: `${props.profit.max_drawdown ? formatPercent(props.profit.max_drawdown, 2) : 'N/A'} (${
|
||||||
|
props.profit.max_drawdown_abs
|
||||||
|
? formatPriceCurrency(
|
||||||
|
props.profit.max_drawdown_abs,
|
||||||
|
props.stakeCurrency,
|
||||||
|
props.stakeCurrencyDecimals,
|
||||||
|
)
|
||||||
|
: 'N/A'
|
||||||
|
}) ${
|
||||||
|
props.profit.max_drawdown_start_timestamp && props.profit.max_drawdown_end_timestamp
|
||||||
|
? 'from ' +
|
||||||
|
timestampms(props.profit.max_drawdown_start_timestamp) +
|
||||||
|
' to ' +
|
||||||
|
timestampms(props.profit.max_drawdown_end_timestamp)
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -76,13 +76,20 @@
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<BotProfit
|
||||||
|
class="mx-1"
|
||||||
|
:profit="botStore.activeBot.profit"
|
||||||
|
:stake-currency="botStore.activeBot.botState.stake_currency"
|
||||||
|
:stake-currency-decimals="botStore.activeBot.botState.stake_currency_decimals ?? 3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
|
||||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||||
|
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
||||||
|
|
||||||
|
import BotProfit from '@/components/ftbot/BotProfit.vue';
|
||||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
|
|
||||||
const botStore = useBotStore();
|
const botStore = useBotStore();
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="me-auto h3">Daily Stats</label>
|
|
||||||
<b-button class="float-end" size="sm" @click="botStore.activeBot.getDaily">
|
|
||||||
<i-mdi-refresh />
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DailyChart
|
|
||||||
v-if="botStore.activeBot.dailyStats.data"
|
|
||||||
:daily-stats="botStore.activeBot.dailyStatsSorted"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<b-table class="table-sm" :items="botStore.activeBot.dailyStats.data" :fields="dailyFields">
|
|
||||||
</b-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<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';
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
onMounted(() => {
|
|
||||||
botStore.activeBot.getDaily();
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,21 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- TODO We could move the list into a component since we are reusing the same code for both lists. -->
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Whitelist Methods</h3>
|
<h3>Whitelist Methods</h3>
|
||||||
|
|
||||||
<div v-if="botStore.activeBot.pairlistMethods.length" class="list">
|
<div v-if="botStore.activeBot.pairlistMethods.length" class="list wide">
|
||||||
<b-list-group v-for="(method, key) in botStore.activeBot.pairlistMethods" :key="key">
|
<div
|
||||||
<b-list-group-item href="#" class="pair white">{{ method }}</b-list-group-item>
|
v-for="(method, key) in botStore.activeBot.pairlistMethods"
|
||||||
</b-list-group>
|
:key="key"
|
||||||
|
class="pair white align-middle border border-secondary"
|
||||||
|
>
|
||||||
|
{{ method }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Show Whitelist -->
|
<!-- Show Whitelist -->
|
||||||
<h3 :title="`${botStore.activeBot.whitelist.length} pairs`">Whitelist</h3>
|
<h3 :title="`${botStore.activeBot.whitelist.length} pairs`">Whitelist</h3>
|
||||||
<div v-if="botStore.activeBot.whitelist.length" class="list">
|
<div v-if="botStore.activeBot.whitelist.length" class="list">
|
||||||
<b-list-group v-for="(pair, key) in botStore.activeBot.whitelist" :key="key">
|
<div
|
||||||
<b-list-group-item class="pair white">{{ pair }}</b-list-group-item>
|
v-for="(pair, key) in botStore.activeBot.whitelist"
|
||||||
</b-list-group>
|
:key="key"
|
||||||
|
class="pair white align-middle border border-secondary text-small"
|
||||||
|
>
|
||||||
|
{{ pair }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else>List Unavailable. Please Login and make sure server is running.</p>
|
<p v-else>List Unavailable. Please Login and make sure server is running.</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -76,14 +83,15 @@
|
||||||
</b-popover>
|
</b-popover>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="botStore.activeBot.blacklist.length" class="list">
|
<div v-if="botStore.activeBot.blacklist.length" class="list">
|
||||||
<b-list-group v-for="(pair, key) in botStore.activeBot.blacklist" :key="key">
|
<div
|
||||||
<b-list-group-item
|
v-for="(pair, key) in botStore.activeBot.blacklist"
|
||||||
class="pair black"
|
:key="key"
|
||||||
:active="blacklistSelect.indexOf(key) > -1"
|
class="pair black border border-secondary"
|
||||||
|
:class="blacklistSelect.indexOf(key) > -1 ? 'active' : ''"
|
||||||
@click="blacklistSelectClick(key)"
|
@click="blacklistSelectClick(key)"
|
||||||
><span class="check"><i-mdi-check-circle /></span>{{ pair }}</b-list-group-item
|
|
||||||
>
|
>
|
||||||
</b-list-group>
|
<span class="check"><i-mdi-check-circle /></span>{{ pair }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else>BlackList Unavailable. Please Login and make sure server is running.</p>
|
<p v-else>BlackList Unavailable. Please Login and make sure server is running.</p>
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
|
@ -119,7 +127,6 @@ const addBlacklistPair = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const blacklistSelectClick = (key) => {
|
const blacklistSelectClick = (key) => {
|
||||||
console.log(key);
|
|
||||||
const index = blacklistSelect.value.indexOf(key);
|
const index = blacklistSelect.value.indexOf(key);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
blacklistSelect.value.splice(index, 1);
|
blacklistSelect.value.splice(index, 1);
|
||||||
|
@ -162,7 +169,7 @@ onMounted(() => {
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item.active .check {
|
.pair.active .check {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,14 +179,14 @@ onMounted(() => {
|
||||||
grid-gap: 0.5rem;
|
grid-gap: 0.5rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
.wide {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.pair {
|
.pair {
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: #41b883;
|
background: #41b883;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
class="btn-xs ms-1"
|
class="btn-xs ms-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Delete trade"
|
title="Delete trade"
|
||||||
@click="removePairLock(row.item)"
|
@click="removePairLock(row.item as unknown as Lock)"
|
||||||
>
|
>
|
||||||
<i-mdi-delete />
|
<i-mdi-delete />
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
v-model="pairlistStore.configName"
|
v-model="pairlistStore.configName"
|
||||||
size="sm"
|
size="sm"
|
||||||
:options="pairlistStore.savedConfigs.map((c) => c.name)"
|
:options="pairlistStore.savedConfigs.map((c) => c.name)"
|
||||||
@change="(config) => pairlistStore.selectOrCreateConfig(config)"
|
@change="(config) => pairlistStore.selectOrCreateConfig(config as string)"
|
||||||
/>
|
/>
|
||||||
</edit-value>
|
</edit-value>
|
||||||
<b-button
|
<b-button
|
||||||
|
|
110
src/components/ftbot/PeriodBreakdown.vue
Normal file
110
src/components/ftbot/PeriodBreakdown.vue
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="me-auto h3">{{ hasWeekly ? 'Period' : 'Daily' }} Breakdown</label>
|
||||||
|
<b-button class="float-end" size="sm" @click="refreshSummary">
|
||||||
|
<i-mdi-refresh />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
<b-form-radio-group
|
||||||
|
v-if="hasWeekly"
|
||||||
|
id="order-direction"
|
||||||
|
v-model="periodicBreakdownPeriod"
|
||||||
|
:options="periodicBreakdownSelections"
|
||||||
|
name="radios-btn-default"
|
||||||
|
size="sm"
|
||||||
|
buttons
|
||||||
|
style="min-width: 10em"
|
||||||
|
button-variant="outline-primary"
|
||||||
|
@change="refreshSummary"
|
||||||
|
></b-form-radio-group>
|
||||||
|
|
||||||
|
<div class="ps-1">
|
||||||
|
<TimePeriodChart
|
||||||
|
v-if="selectedStats"
|
||||||
|
:daily-stats="selectedStatsSorted"
|
||||||
|
:show-title="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b-table class="table-sm" :items="selectedStats.data" :fields="dailyFields"> </b-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TimePeriodChart from '@/components/charts/TimePeriodChart.vue';
|
||||||
|
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||||
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
|
import { TableField } from 'bootstrap-vue-next';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { TimeSummaryOptions } from '@/types';
|
||||||
|
|
||||||
|
const botStore = useBotStore();
|
||||||
|
|
||||||
|
const hasWeekly = computed(() => botStore.activeBot.botApiVersion >= 2.33);
|
||||||
|
|
||||||
|
const periodicBreakdownSelections = computed(() => {
|
||||||
|
const vals = [{ value: TimeSummaryOptions.daily, text: 'Days' }];
|
||||||
|
if (hasWeekly.value) {
|
||||||
|
vals.push({ value: TimeSummaryOptions.weekly, text: 'Weeks' });
|
||||||
|
vals.push({ value: TimeSummaryOptions.monthly, text: 'Months' });
|
||||||
|
}
|
||||||
|
return vals;
|
||||||
|
});
|
||||||
|
const periodicBreakdownPeriod = ref<TimeSummaryOptions>(TimeSummaryOptions.daily);
|
||||||
|
|
||||||
|
const selectedStats = computed(() => {
|
||||||
|
switch (periodicBreakdownPeriod.value) {
|
||||||
|
case TimeSummaryOptions.weekly:
|
||||||
|
return botStore.activeBot.weeklyStats;
|
||||||
|
case TimeSummaryOptions.monthly:
|
||||||
|
return botStore.activeBot.monthlyStats;
|
||||||
|
}
|
||||||
|
return botStore.activeBot.dailyStats;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedStatsSorted = computed(() => {
|
||||||
|
// Sorted version for chart
|
||||||
|
return {
|
||||||
|
...selectedStats.value,
|
||||||
|
data: selectedStats.value.data
|
||||||
|
? Object.values(selectedStats.value.data).sort((a, b) => (a.date > b.date ? 1 : -1))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailyFields = computed<TableField[]>(() => {
|
||||||
|
const res: TableField[] = [
|
||||||
|
{ key: 'date', label: 'Day' },
|
||||||
|
{
|
||||||
|
key: 'abs_profit',
|
||||||
|
label: 'Profit',
|
||||||
|
formatter: (value: unknown) =>
|
||||||
|
formatPrice(value as number, botStore.activeBot.stakeCurrencyDecimals),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
function refreshSummary() {
|
||||||
|
botStore.activeBot.getTimeSummary(periodicBreakdownPeriod.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshSummary();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -61,20 +61,24 @@ const timeRange = computed(() => {
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateInput = () => {
|
function updateInput() {
|
||||||
const tr = props.modelValue.split('-');
|
const tr = props.modelValue.split('-');
|
||||||
if (tr[0]) {
|
if (tr[0]) {
|
||||||
dateFrom.value = timestampToDateString(dateFromString(tr[0], 'yyyyMMdd'));
|
dateFrom.value = timestampToDateString(
|
||||||
|
tr[0].length === 8 ? dateFromString(tr[0], 'yyyyMMdd') : parseInt(tr[0]) * 1000,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dateFrom.value = '';
|
dateFrom.value = '';
|
||||||
}
|
}
|
||||||
if (tr.length > 1 && tr[1]) {
|
if (tr.length > 1 && tr[1]) {
|
||||||
dateTo.value = timestampToDateString(dateFromString(tr[1], 'yyyyMMdd'));
|
dateTo.value = timestampToDateString(
|
||||||
|
tr[1].length === 8 ? dateFromString(tr[1], 'yyyyMMdd') : parseInt(tr[1]) * 1000,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dateTo.value = '';
|
dateTo.value = '';
|
||||||
}
|
}
|
||||||
emit('update:modelValue', timeRange.value);
|
emit('update:modelValue', timeRange.value);
|
||||||
};
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => timeRange.value,
|
() => timeRange.value,
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<i-mdi-close-box-multiple class="me-1" />Forceexit partial
|
<i-mdi-close-box-multiple class="me-1" />Forceexit partial
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button
|
<b-button
|
||||||
v-if="botApiVersion >= 2.24 && trade.open_order_id"
|
v-if="botApiVersion >= 2.24 && (trade.open_order_id || trade.has_open_orders)"
|
||||||
class="btn-xs text-start mt-1"
|
class="btn-xs text-start mt-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Cancel open orders"
|
title="Cancel open orders"
|
||||||
|
|
|
@ -26,12 +26,12 @@
|
||||||
@row-clicked="onRowClicked"
|
@row-clicked="onRowClicked"
|
||||||
@row-selected="onRowSelected"
|
@row-selected="onRowSelected"
|
||||||
>
|
>
|
||||||
<template #cell(actions)="row">
|
<template #cell(actions)="{ index, item }">
|
||||||
<TradeActionsPopover
|
<TradeActionsPopover
|
||||||
:id="row.index"
|
:id="index"
|
||||||
:trade="row.item"
|
:trade="item as unknown as Trade"
|
||||||
:bot-api-version="botStore.activeBot.botApiVersion"
|
:bot-api-version="botStore.activeBot.botApiVersion"
|
||||||
@delete-trade="removeTradeHandler(row.item)"
|
@delete-trade="removeTradeHandler(item as unknown as Trade)"
|
||||||
@force-exit="forceExitHandler"
|
@force-exit="forceExitHandler"
|
||||||
@force-exit-partial="forceExitPartialHandler"
|
@force-exit-partial="forceExitPartialHandler"
|
||||||
@cancel-open-order="cancelOpenOrderHandler"
|
@cancel-open-order="cancelOpenOrderHandler"
|
||||||
|
@ -40,11 +40,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #cell(pair)="row">
|
<template #cell(pair)="row">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{ `${row.item.pair}${row.item.open_order_id || row.item.has_open_orders ? '*' : ''}` }}
|
||||||
`${row.item.pair}${
|
|
||||||
row.item.open_order_id === undefined || row.item.open_order_id === null ? '' : '*'
|
|
||||||
}`
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(trade_id)="row">
|
<template #cell(trade_id)="row">
|
||||||
|
@ -60,13 +56,13 @@
|
||||||
{{ row.item.trading_mode !== 'spot' ? `(${row.item.leverage}x)` : '' }}
|
{{ row.item.trading_mode !== 'spot' ? `(${row.item.leverage}x)` : '' }}
|
||||||
</template>
|
</template>
|
||||||
<template #cell(profit)="row">
|
<template #cell(profit)="row">
|
||||||
<trade-profit :trade="row.item" />
|
<trade-profit :trade="row.item as unknown as Trade" />
|
||||||
</template>
|
</template>
|
||||||
<template #cell(open_timestamp)="row">
|
<template #cell(open_timestamp)="row">
|
||||||
<DateTimeTZ :date="row.item.open_timestamp" />
|
<DateTimeTZ :date="(row.item as unknown as Trade).open_timestamp" />
|
||||||
</template>
|
</template>
|
||||||
<template #cell(close_timestamp)="row">
|
<template #cell(close_timestamp)="row">
|
||||||
<DateTimeTZ :date="row.item.close_timestamp" />
|
<DateTimeTZ :date="(row.item as unknown as Trade).close_timestamp ?? 0" />
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</b-table>
|
||||||
<div class="w-100 d-flex justify-content-between">
|
<div class="w-100 d-flex justify-content-between">
|
||||||
|
@ -97,7 +93,7 @@ import TradeProfit from './TradeProfit.vue';
|
||||||
import TradeActionsPopover from './TradeActionsPopover.vue';
|
import TradeActionsPopover from './TradeActionsPopover.vue';
|
||||||
import ForceExitForm from '@/components/ftbot/ForceExitForm.vue';
|
import ForceExitForm from '@/components/ftbot/ForceExitForm.vue';
|
||||||
|
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { TableField, TableItem } from 'bootstrap-vue-next';
|
import { TableField, TableItem } from 'bootstrap-vue-next';
|
||||||
|
@ -143,7 +139,12 @@ const rows = computed(() => {
|
||||||
return props.trades.length;
|
return props.trades.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableFields: TableField[] = [
|
// This using "TableField[]" below causes
|
||||||
|
// Error: Debug Failure. No error for last overload signature
|
||||||
|
const tableFields = ref<any[]>([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
tableFields.value = [
|
||||||
{ key: 'trade_id', label: 'ID' },
|
{ key: 'trade_id', label: 'ID' },
|
||||||
{ key: 'pair', label: 'Pair' },
|
{ key: 'pair', label: 'Pair' },
|
||||||
{ key: 'amount', label: 'Amount' },
|
{ key: 'amount', label: 'Amount' },
|
||||||
|
@ -164,7 +165,6 @@ const tableFields: TableField[] = [
|
||||||
{
|
{
|
||||||
key: 'profit',
|
key: 'profit',
|
||||||
label: props.activeTrades ? 'Current profit %' : 'Profit %',
|
label: props.activeTrades ? 'Current profit %' : 'Profit %',
|
||||||
|
|
||||||
formatter: (value: unknown, key?: string, item?: unknown) => {
|
formatter: (value: unknown, key?: string, item?: unknown) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -178,8 +178,9 @@ const tableFields: TableField[] = [
|
||||||
...(props.activeTrades ? openFields : closedFields),
|
...(props.activeTrades ? openFields : closedFields),
|
||||||
];
|
];
|
||||||
if (props.multiBotView) {
|
if (props.multiBotView) {
|
||||||
tableFields.unshift({ key: 'botName', label: 'Bot' });
|
tableFields.value.unshift({ key: 'botName', label: 'Bot' });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const feOrderType = ref<string | undefined>(undefined);
|
const feOrderType = ref<string | undefined>(undefined);
|
||||||
const forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => {
|
const forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => {
|
||||||
|
|
|
@ -9,26 +9,48 @@
|
||||||
>Trade Navigation {{ sortNewestFirst ? '↓' : '↑' }}
|
>Trade Navigation {{ sortNewestFirst ? '↓' : '↑' }}
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
<b-list-group-item
|
<b-list-group-item
|
||||||
v-for="trade in sortedTrades"
|
v-for="(trade, i) in sortedTrades"
|
||||||
:key="trade.open_timestamp"
|
:key="trade.open_timestamp"
|
||||||
button
|
button
|
||||||
class="d-flex flex-wrap justify-content-between align-items-center py-1"
|
class="d-flex flex-column py-1 pe-1 align-items-stretch"
|
||||||
:title="`${trade.pair}`"
|
:title="`${trade.pair}`"
|
||||||
:active="trade.open_timestamp === selectedTrade.open_timestamp"
|
:active="trade.open_timestamp === selectedTrade.open_timestamp"
|
||||||
@click="onTradeSelect(trade)"
|
@click="onTradeSelect(trade)"
|
||||||
>
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
<div>
|
<div>
|
||||||
<span v-if="botStore.activeBot.botState.trading_mode !== 'spot'">{{
|
<span v-if="botStore.activeBot.botState.trading_mode !== 'spot'">{{
|
||||||
trade.is_short ? 'S-' : 'L-'
|
trade.is_short ? 'S-' : 'L-'
|
||||||
}}</span>
|
}}</span>
|
||||||
<DateTimeTZ :date="trade.open_timestamp" />
|
<DateTimeTZ :date="trade.open_timestamp" />
|
||||||
</div>
|
</div>
|
||||||
<TradeProfit :trade="trade" />
|
<TradeProfit :trade="trade" class="my-1" />
|
||||||
<ProfitPill
|
<ProfitPill
|
||||||
v-if="backtestMode"
|
v-if="backtestMode"
|
||||||
:profit-ratio="trade.profit_ratio"
|
:profit-ratio="trade.profit_ratio"
|
||||||
:stake-currency="botStore.activeBot.stakeCurrency"
|
:stake-currency="botStore.activeBot.stakeCurrency"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<b-button
|
||||||
|
size="sm"
|
||||||
|
class="ms-auto"
|
||||||
|
variant="outline-secondary"
|
||||||
|
@click="ordersVisible[i] = !ordersVisible[i]"
|
||||||
|
><i-mdi-chevron-right v-if="!ordersVisible[i]" width="24" height="24" />
|
||||||
|
<i-mdi-chevron-down v-if="ordersVisible[i]" width="24" height="24" />
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
<b-collapse v-model="ordersVisible[i]">
|
||||||
|
<ul class="px-3 m-0">
|
||||||
|
<li
|
||||||
|
v-for="order in trade.orders?.filter((o) => o.order_filled_timestamp !== null)"
|
||||||
|
:key="order.order_timestamp"
|
||||||
|
>
|
||||||
|
{{ order.ft_order_side }} {{ order.amount }} at {{ order.safe_price }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</b-collapse>
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
<b-list-group-item v-if="trades.length === 0">No trades to show...</b-list-group-item>
|
<b-list-group-item v-if="trades.length === 0">No trades to show...</b-list-group-item>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
|
@ -39,7 +61,7 @@
|
||||||
import { Trade } from '@/types';
|
import { Trade } from '@/types';
|
||||||
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
||||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||||
|
|
||||||
|
@ -67,6 +89,15 @@ const sortedTrades = computed(() => {
|
||||||
: a.open_timestamp - b.open_timestamp,
|
: a.open_timestamp - b.open_timestamp,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ordersVisible = ref(sortedTrades.value.map(() => false));
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => botStore.activeBot.selectedPair,
|
||||||
|
() => {
|
||||||
|
ordersVisible.value = sortedTrades.value.map(() => false);
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
|
import {
|
||||||
|
timestampmsOrNa,
|
||||||
|
timestampmsWithTimezone,
|
||||||
|
timestampToDateString,
|
||||||
|
} from '@/shared/formatters';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
@ -19,7 +23,7 @@ const formattedDate = computed((): string => {
|
||||||
if (props.showTimezone) {
|
if (props.showTimezone) {
|
||||||
return timestampmsWithTimezone(props.date);
|
return timestampmsWithTimezone(props.date);
|
||||||
}
|
}
|
||||||
return timestampms(props.date);
|
return timestampmsOrNa(props.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
const timezoneTooltip = computed((): string => {
|
const timezoneTooltip = computed((): string => {
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
@ -104,8 +104,11 @@ enum EditState {
|
||||||
Duplicating,
|
Duplicating,
|
||||||
}
|
}
|
||||||
|
|
||||||
const localName = ref<string>(props.modelValue);
|
const localName = ref<string>('');
|
||||||
const mode = ref<EditState>(EditState.None);
|
const mode = ref<EditState>(EditState.None);
|
||||||
|
onMounted(() => {
|
||||||
|
localName.value = props.modelValue;
|
||||||
|
});
|
||||||
|
|
||||||
function abort() {
|
function abort() {
|
||||||
mode.value = EditState.None;
|
mode.value = EditState.None;
|
||||||
|
|
|
@ -42,17 +42,17 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/open_trades',
|
path: '/open_trades',
|
||||||
component: () => import('@/views/TradesList.vue'),
|
component: () => import('@/views/TradesListView.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/trade_history',
|
path: '/trade_history',
|
||||||
component: () => import('@/views/TradesList.vue'),
|
component: () => import('@/views/TradesListView.vue'),
|
||||||
props: { history: true },
|
props: { history: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/pairlist',
|
path: '/pairlist',
|
||||||
component: () => import('@/components/ftbot/FTBotAPIPairList.vue'),
|
component: () => import('@/components/ftbot/PairListLive.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
|
|
|
@ -12,11 +12,14 @@ export function calculateDiff(
|
||||||
): number[][] {
|
): number[][] {
|
||||||
const fromIdx = columns.indexOf(colFrom);
|
const fromIdx = columns.indexOf(colFrom);
|
||||||
const toIdx = columns.indexOf(colTo);
|
const toIdx = columns.indexOf(colTo);
|
||||||
|
const hasBothColumns = fromIdx > 0 && toIdx > 0;
|
||||||
|
if (hasBothColumns) {
|
||||||
columns.push(`${colFrom}-${colTo}`);
|
columns.push(`${colFrom}-${colTo}`);
|
||||||
|
}
|
||||||
return data.map((original) => {
|
return data.map((original) => {
|
||||||
// Prevent mutation of original data
|
// Prevent mutation of original data
|
||||||
const candle = original.slice();
|
const candle = original.slice();
|
||||||
|
if (hasBothColumns) {
|
||||||
const diff =
|
const diff =
|
||||||
candle === null || candle[toIdx] === null || candle[fromIdx] === null
|
candle === null || candle[toIdx] === null || candle[fromIdx] === null
|
||||||
? null
|
? null
|
||||||
|
@ -24,6 +27,7 @@ export function calculateDiff(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
candle.push(diff);
|
candle.push(diff);
|
||||||
|
}
|
||||||
return candle;
|
return candle;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,11 +47,12 @@ export function getTradeEntries(dataset: PairHistory, trades: Trade[]) {
|
||||||
// 4: color
|
// 4: color
|
||||||
// 5: label
|
// 5: label
|
||||||
// 6: tooltip
|
// 6: tooltip
|
||||||
|
const stop_ts_adjusted = dataset.data_stop_ts + dataset.timeframe_ms;
|
||||||
for (let i = 0, len = trades.length; i < len; i += 1) {
|
for (let i = 0, len = trades.length; i < len; i += 1) {
|
||||||
const trade: Trade = trades[i];
|
const trade: Trade = trades[i];
|
||||||
if (
|
if (
|
||||||
// Trade is open or closed and within timerange
|
// Trade is open or closed and within timerange
|
||||||
roundTimeframe(dataset.timeframe_ms ?? 0, trade.open_timestamp) <= dataset.data_stop_ts ||
|
roundTimeframe(dataset.timeframe_ms ?? 0, trade.open_timestamp) <= stop_ts_adjusted ||
|
||||||
!trade.close_timestamp ||
|
!trade.close_timestamp ||
|
||||||
(trade.close_timestamp && trade.close_timestamp >= dataset.data_start_ts)
|
(trade.close_timestamp && trade.close_timestamp >= dataset.data_start_ts)
|
||||||
) {
|
) {
|
||||||
|
@ -61,7 +62,7 @@ export function getTradeEntries(dataset: PairHistory, trades: Trade[]) {
|
||||||
if (
|
if (
|
||||||
order.order_filled_timestamp &&
|
order.order_filled_timestamp &&
|
||||||
roundTimeframe(dataset.timeframe_ms ?? 0, order.order_filled_timestamp) <=
|
roundTimeframe(dataset.timeframe_ms ?? 0, order.order_filled_timestamp) <=
|
||||||
dataset.data_stop_ts &&
|
stop_ts_adjusted &&
|
||||||
order.order_filled_timestamp > dataset.data_start_ts
|
order.order_filled_timestamp > dataset.data_start_ts
|
||||||
) {
|
) {
|
||||||
// Trade entry
|
// Trade entry
|
||||||
|
@ -79,7 +80,7 @@ export function getTradeEntries(dataset: PairHistory, trades: Trade[]) {
|
||||||
} else if (i === trade.orders.length - 1 && trade.close_timestamp) {
|
} else if (i === trade.orders.length - 1 && trade.close_timestamp) {
|
||||||
if (
|
if (
|
||||||
roundTimeframe(dataset.timeframe_ms ?? 0, trade.close_timestamp) <=
|
roundTimeframe(dataset.timeframe_ms ?? 0, trade.close_timestamp) <=
|
||||||
dataset.data_stop_ts &&
|
stop_ts_adjusted &&
|
||||||
trade.close_timestamp > dataset.data_start_ts &&
|
trade.close_timestamp > dataset.data_start_ts &&
|
||||||
trade.is_open === false
|
trade.is_open === false
|
||||||
) {
|
) {
|
||||||
|
@ -127,6 +128,8 @@ export function generateTradeSeries(
|
||||||
): ScatterSeriesOption {
|
): ScatterSeriesOption {
|
||||||
const { tradeData } = getTradeEntries(dataset, trades);
|
const { tradeData } = getTradeEntries(dataset, trades);
|
||||||
|
|
||||||
|
const openTrades = trades.filter((t) => t.is_open);
|
||||||
|
|
||||||
const tradesSeries: ScatterSeriesOption = {
|
const tradesSeries: ScatterSeriesOption = {
|
||||||
name: nameTrades,
|
name: nameTrades,
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
|
@ -158,6 +161,41 @@ export function generateTradeSeries(
|
||||||
symbolSize: 13,
|
symbolSize: 13,
|
||||||
data: tradeData,
|
data: tradeData,
|
||||||
};
|
};
|
||||||
|
// Show distance to stoploss
|
||||||
|
if (openTrades.length > 0) {
|
||||||
|
// Ensure to import and "use" whatever feature in candleChart! (MarkLine, MarkArea, ...)
|
||||||
|
// Offset to avoid having the line at the very end of the chart
|
||||||
|
const offset = dataset.timeframe_ms * 10;
|
||||||
|
|
||||||
|
tradesSeries.markLine = {
|
||||||
|
symbol: 'none',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ff0000AA',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'middle',
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
type: 'solid',
|
||||||
|
},
|
||||||
|
data: openTrades.map((t) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Stoploss',
|
||||||
|
yAxis: t.stop_loss_abs,
|
||||||
|
xAxis:
|
||||||
|
dataset.data_stop_ts - offset > t.open_timestamp
|
||||||
|
? t.open_timestamp
|
||||||
|
: dataset.data_stop_ts - offset,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
yAxis: t.stop_loss_abs,
|
||||||
|
xAxis: t.close_timestamp ?? dataset.data_stop_ts + dataset.timeframe_ms,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
return tradesSeries;
|
return tradesSeries;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export function formatPrice(value: number | null, decimals = 15): string {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function formatPriceCurrency(price: number | null, currency: string, decimals = 3) {
|
export function formatPriceCurrency(price: number | null, currency: string, decimals = 3) {
|
||||||
return `${formatPrice(price, decimals)} ${currency}`;
|
return `${formatPrice(price, decimals)} ${currency ?? ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -38,13 +38,25 @@ export function timestampms(ts: number | Date): string {
|
||||||
return formatDate(toDate(ts), 'yyyy-MM-dd HH:mm:ss');
|
return formatDate(toDate(ts), 'yyyy-MM-dd HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a timestamp / Date object to String.
|
||||||
|
* Returns 'N/A' if ts is null
|
||||||
|
* @param ts Timestamp as number or date (in utc!!)
|
||||||
|
*/
|
||||||
|
export function timestampmsOrNa(ts: number | Date | null): string {
|
||||||
|
return ts ? formatDate(toDate(ts), 'yyyy-MM-dd HH:mm:ss') : 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a timestamp / Date object to String
|
* Convert a timestamp / Date object to String
|
||||||
* @param ts Timestamp as number or date (in utc!!)
|
* @param ts Timestamp as number or date (in utc!!)
|
||||||
* @param timezone timezone to use
|
* @param timezone timezone to use
|
||||||
* @returns formatted date in desired timezone (or globally configured timezone)
|
* @returns formatted date in desired timezone (or globally configured timezone)
|
||||||
*/
|
*/
|
||||||
export function timestampmsWithTimezone(ts: number | Date, timezone?: string): string {
|
export function timestampmsWithTimezone(ts: number | Date | null, timezone?: string): string {
|
||||||
|
if (!ts) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
return formatDate(toDate(ts), 'yyyy-MM-dd HH:mm:ss (z)', timezone);
|
return formatDate(toDate(ts), 'yyyy-MM-dd HH:mm:ss (z)', timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -232,7 +232,6 @@ export class UserService {
|
||||||
* Call on startup to migrate old login info to new login
|
* Call on startup to migrate old login info to new login
|
||||||
*/
|
*/
|
||||||
public static migrateLogin() {
|
public static migrateLogin() {
|
||||||
// TODO: this is actually never called!
|
|
||||||
const AUTH_REFRESH_TOKEN = 'auth_ref_token'; // Legacy key - do not use
|
const AUTH_REFRESH_TOKEN = 'auth_ref_token'; // Legacy key - do not use
|
||||||
const AUTH_ACCESS_TOKEN = 'auth_access_token';
|
const AUTH_ACCESS_TOKEN = 'auth_access_token';
|
||||||
const AUTH_API_URL = 'auth_api_url';
|
const AUTH_API_URL = 'auth_api_url';
|
||||||
|
|
25
src/stores/btStore.ts
Normal file
25
src/stores/btStore.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useBtStore = defineStore('btStore', {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
strategy: '',
|
||||||
|
selectedTimeframe: '',
|
||||||
|
selectedDetailTimeframe: '',
|
||||||
|
timerange: '',
|
||||||
|
maxOpenTrades: '',
|
||||||
|
stakeAmount: '',
|
||||||
|
startingCapital: '',
|
||||||
|
allowCache: true,
|
||||||
|
enableProtections: false,
|
||||||
|
stakeAmountUnlimited: false,
|
||||||
|
freqAI: {
|
||||||
|
enabled: false,
|
||||||
|
model: '',
|
||||||
|
identifier: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {},
|
||||||
|
});
|
|
@ -6,18 +6,17 @@ import {
|
||||||
PlotConfig,
|
PlotConfig,
|
||||||
StrategyResult,
|
StrategyResult,
|
||||||
BalanceInterface,
|
BalanceInterface,
|
||||||
DailyReturnValue,
|
TimeSummaryReturnValue,
|
||||||
LockResponse,
|
LockResponse,
|
||||||
ProfitInterface,
|
ProfitInterface,
|
||||||
BacktestResult,
|
BacktestResult,
|
||||||
StrategyBacktestResult,
|
|
||||||
BacktestSteps,
|
BacktestSteps,
|
||||||
LogLine,
|
LogLine,
|
||||||
SysInfoResponse,
|
SysInfoResponse,
|
||||||
LoadingStatus,
|
LoadingStatus,
|
||||||
BacktestHistoryEntry,
|
BacktestHistoryEntry,
|
||||||
RunModes,
|
RunModes,
|
||||||
DailyPayload,
|
TimeSummaryPayload,
|
||||||
BlacklistResponse,
|
BlacklistResponse,
|
||||||
WhitelistResponse,
|
WhitelistResponse,
|
||||||
StrategyListResult,
|
StrategyListResult,
|
||||||
|
@ -43,6 +42,11 @@ import {
|
||||||
PairlistEvalResponse,
|
PairlistEvalResponse,
|
||||||
PairlistsPayload,
|
PairlistsPayload,
|
||||||
PairlistsResponse,
|
PairlistsResponse,
|
||||||
|
BacktestResultInMemory,
|
||||||
|
BacktestMetadataWithStrategyName,
|
||||||
|
BacktestMetadataPatch,
|
||||||
|
BacktestResultUpdate,
|
||||||
|
TimeSummaryOptions,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
@ -78,7 +82,9 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
profit: {} as ProfitInterface,
|
profit: {} as ProfitInterface,
|
||||||
botState: {} as BotState,
|
botState: {} as BotState,
|
||||||
balance: {} as BalanceInterface,
|
balance: {} as BalanceInterface,
|
||||||
dailyStats: {} as DailyReturnValue,
|
dailyStats: {} as TimeSummaryReturnValue,
|
||||||
|
weeklyStats: {} as TimeSummaryReturnValue,
|
||||||
|
monthlyStats: {} as TimeSummaryReturnValue,
|
||||||
pairlistMethods: [] as string[],
|
pairlistMethods: [] as string[],
|
||||||
detailTradeId: null as number | null,
|
detailTradeId: null as number | null,
|
||||||
selectedPair: '',
|
selectedPair: '',
|
||||||
|
@ -102,7 +108,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
backtestTradeCount: 0,
|
backtestTradeCount: 0,
|
||||||
backtestResult: undefined as BacktestResult | undefined,
|
backtestResult: undefined as BacktestResult | undefined,
|
||||||
selectedBacktestResultKey: '',
|
selectedBacktestResultKey: '',
|
||||||
backtestHistory: {} as Record<string, StrategyBacktestResult>,
|
backtestHistory: {} as Record<string, BacktestResultInMemory>,
|
||||||
backtestHistoryList: [] as BacktestHistoryEntry[],
|
backtestHistoryList: [] as BacktestHistoryEntry[],
|
||||||
sysInfo: {} as SysInfoResponse,
|
sysInfo: {} as SysInfoResponse,
|
||||||
};
|
};
|
||||||
|
@ -114,7 +120,8 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
stakeCurrencyDecimals: (state) => state.botState?.stake_currency_decimals || 3,
|
stakeCurrencyDecimals: (state) => state.botState?.stake_currency_decimals || 3,
|
||||||
canRunBacktest: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
canRunBacktest: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
||||||
isWebserverMode: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
isWebserverMode: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
||||||
selectedBacktestResult: (state) => state.backtestHistory[state.selectedBacktestResultKey],
|
selectedBacktestResult: (state) =>
|
||||||
|
state.backtestHistory[state.selectedBacktestResultKey]?.strategy || {},
|
||||||
shortAllowed: (state) => state.botState?.short_allowed || false,
|
shortAllowed: (state) => state.botState?.short_allowed || false,
|
||||||
openTradeCount: (state) => state.openTrades.length,
|
openTradeCount: (state) => state.openTrades.length,
|
||||||
isTrading: (state) =>
|
isTrading: (state) =>
|
||||||
|
@ -151,14 +158,6 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
botName: (state) => state.botState?.bot_name || 'freqtrade',
|
botName: (state) => state.botState?.bot_name || 'freqtrade',
|
||||||
allTrades: (state) => [...state.openTrades, ...state.trades] as Trade[],
|
allTrades: (state) => [...state.openTrades, ...state.trades] as Trade[],
|
||||||
activeLocks: (state) => state.currentLocks?.locks || [],
|
activeLocks: (state) => state.currentLocks?.locks || [],
|
||||||
dailyStatsSorted: (state): DailyReturnValue => {
|
|
||||||
return {
|
|
||||||
...state.dailyStats,
|
|
||||||
data: state.dailyStats.data
|
|
||||||
? Object.values(state.dailyStats.data).sort((a, b) => (a.date > b.date ? 1 : -1))
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
botAdded() {
|
botAdded() {
|
||||||
|
@ -168,7 +167,6 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
try {
|
try {
|
||||||
const result = await api.get('/ping');
|
const result = await api.get('/ping');
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// TODO: Name collision!
|
|
||||||
this.ping = `${result.data.status} ${now.toString()}`;
|
this.ping = `${result.data.status} ${now.toString()}`;
|
||||||
this.setIsBotOnline(true);
|
this.setIsBotOnline(true);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -363,30 +361,37 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getPairHistory(payload: PairHistoryPayload) {
|
async getPairHistory(payload: PairHistoryPayload) {
|
||||||
if (payload.pair && payload.timeframe) {
|
if (payload.pair && payload.timeframe) {
|
||||||
this.historyStatus = LoadingStatus.loading;
|
this.historyStatus = LoadingStatus.loading;
|
||||||
return api
|
try {
|
||||||
.get('/pair_history', {
|
const { data } = await api.get('/pair_history', {
|
||||||
params: { ...payload },
|
params: { ...payload },
|
||||||
timeout: 50000,
|
timeout: 50000,
|
||||||
})
|
});
|
||||||
.then((result) => {
|
|
||||||
this.history = {
|
this.history = {
|
||||||
[`${payload.pair}__${payload.timeframe}`]: {
|
[`${payload.pair}__${payload.timeframe}`]: {
|
||||||
pair: payload.pair,
|
pair: payload.pair,
|
||||||
timeframe: payload.timeframe,
|
timeframe: payload.timeframe,
|
||||||
timerange: payload.timerange,
|
timerange: payload.timerange,
|
||||||
data: result.data,
|
data: data,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.historyStatus = LoadingStatus.success;
|
this.historyStatus = LoadingStatus.success;
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.historyStatus = LoadingStatus.error;
|
this.historyStatus = LoadingStatus.error;
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
console.error(err.response);
|
||||||
|
const errMsg = err.response?.data?.detail ?? 'Error fetching history';
|
||||||
|
showAlert(errMsg, 'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Error branchs
|
// Error branchs
|
||||||
const error = 'pair or timeframe or timerange not specified';
|
const error = 'pair or timeframe or timerange not specified';
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -436,6 +441,11 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.error(error.response);
|
||||||
|
const errMsg = error.response?.data?.detail ?? 'Error fetching history';
|
||||||
|
showAlert(errMsg, 'warning');
|
||||||
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -520,11 +530,20 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getDaily(payload: DailyPayload = {}) {
|
async getTimeSummary(aggregation: TimeSummaryOptions, payload: TimeSummaryPayload = {}) {
|
||||||
const { timescale = 20 } = payload;
|
const { timescale = 20 } = payload;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<DailyReturnValue>('/daily', { params: { timescale } });
|
const { data } = await api.get<TimeSummaryReturnValue>(`/${aggregation}`, {
|
||||||
|
params: { timescale },
|
||||||
|
});
|
||||||
|
if (aggregation === TimeSummaryOptions.daily) {
|
||||||
this.dailyStats = data;
|
this.dailyStats = data;
|
||||||
|
} else if (aggregation === TimeSummaryOptions.weekly) {
|
||||||
|
this.weeklyStats = data;
|
||||||
|
} else if (aggregation === TimeSummaryOptions.monthly) {
|
||||||
|
this.monthlyStats = data;
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -880,18 +899,30 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getBacktestHistory() {
|
async getBacktestHistory() {
|
||||||
const result = await api.get<BacktestHistoryEntry[]>('/backtest/history');
|
const { data } = await api.get<BacktestHistoryEntry[]>('/backtest/history');
|
||||||
this.backtestHistoryList = result.data;
|
this.backtestHistoryList = data;
|
||||||
},
|
},
|
||||||
updateBacktestResult(backtestResult: BacktestResult) {
|
updateBacktestResult(backtestResult: BacktestResult) {
|
||||||
this.backtestResult = backtestResult;
|
this.backtestResult = backtestResult;
|
||||||
// TODO: Properly identify duplicates to avoid pushing the same multiple times
|
|
||||||
Object.entries(backtestResult.strategy).forEach(([key, strat]) => {
|
Object.entries(backtestResult.strategy).forEach(([key, strat]) => {
|
||||||
console.log(key, strat);
|
const metadata: BacktestMetadataWithStrategyName = {
|
||||||
|
...(backtestResult.metadata[key] ?? {}),
|
||||||
|
strategyName: key,
|
||||||
|
notes: backtestResult.metadata[key]?.notes ?? ``,
|
||||||
|
editing: false,
|
||||||
|
};
|
||||||
|
// console.log(key, strat, metadata);
|
||||||
|
|
||||||
const stratKey = `${key}_${strat.total_trades}_${strat.profit_total.toFixed(3)}`;
|
// Never versions will always have run_id
|
||||||
|
const stratKey =
|
||||||
|
backtestResult.metadata[key].run_id ??
|
||||||
|
`${key}_${strat.total_trades}_${strat.profit_total.toFixed(3)}`;
|
||||||
|
const btResult: BacktestResultInMemory = {
|
||||||
|
metadata,
|
||||||
|
strategy: strat,
|
||||||
|
};
|
||||||
// this.backtestHistory[stratKey] = strat;
|
// this.backtestHistory[stratKey] = strat;
|
||||||
this.backtestHistory = { ...this.backtestHistory, ...{ [stratKey]: strat } };
|
this.backtestHistory = { ...this.backtestHistory, ...{ [stratKey]: btResult } };
|
||||||
this.selectedBacktestResultKey = stratKey;
|
this.selectedBacktestResultKey = stratKey;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -903,9 +934,50 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
this.updateBacktestResult(result.data.backtest_result);
|
this.updateBacktestResult(result.data.backtest_result);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async deleteBacktestHistoryResult(btHistoryEntry: BacktestHistoryEntry) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.delete<BacktestHistoryEntry[]>(
|
||||||
|
`/backtest/history/${btHistoryEntry.filename}`,
|
||||||
|
);
|
||||||
|
this.backtestHistoryList = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveBacktestResultMetadata(payload: BacktestResultUpdate) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.patch<
|
||||||
|
BacktestMetadataPatch,
|
||||||
|
AxiosResponse<BacktestHistoryEntry[]>
|
||||||
|
>(`/backtest/history/${payload.filename}`, payload);
|
||||||
|
console.log(data);
|
||||||
|
data.forEach((entry) => {
|
||||||
|
if (entry.run_id in this.backtestHistory) {
|
||||||
|
this.backtestHistory[entry.run_id].metadata.notes = entry.notes;
|
||||||
|
console.log('updating ...');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Update metadata in backtestHistoryList
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
setBacktestResultKey(key: string) {
|
setBacktestResultKey(key: string) {
|
||||||
this.selectedBacktestResultKey = key;
|
this.selectedBacktestResultKey = key;
|
||||||
},
|
},
|
||||||
|
removeBacktestResultFromMemory(key: string) {
|
||||||
|
if (this.selectedBacktestResultKey === key) {
|
||||||
|
// Get first key from backtestHistory that is not the key to be deleted
|
||||||
|
const keys = Object.keys(this.backtestHistory);
|
||||||
|
const index = keys.findIndex((k) => k !== key);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.selectedBacktestResultKey = keys[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete this.backtestHistory[key];
|
||||||
|
},
|
||||||
async getSysInfo() {
|
async getSysInfo() {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<SysInfoResponse>('/sysinfo');
|
const { data } = await api.get<SysInfoResponse>('/sysinfo');
|
||||||
|
@ -936,7 +1008,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
// TODO: check for active bot ...
|
// TODO: check for active bot ...
|
||||||
if (pair === this.selectedPair) {
|
if (pair === this.selectedPair) {
|
||||||
// Reload pair candles
|
// Reload pair candles
|
||||||
this.getPairCandles({ pair, timeframe, limit: 500 });
|
this.getPairCandles({ pair, timeframe });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,16 @@ import {
|
||||||
BotDescriptors,
|
BotDescriptors,
|
||||||
BotState,
|
BotState,
|
||||||
ClosedTrade,
|
ClosedTrade,
|
||||||
DailyPayload,
|
TimeSummaryPayload,
|
||||||
DailyRecord,
|
TimeSummaryRecord,
|
||||||
DailyReturnValue,
|
TimeSummaryReturnValue,
|
||||||
MultiCancelOpenOrderPayload,
|
MultiCancelOpenOrderPayload,
|
||||||
MultiDeletePayload,
|
MultiDeletePayload,
|
||||||
MultiForcesellPayload,
|
MultiForcesellPayload,
|
||||||
MultiReloadTradePayload,
|
MultiReloadTradePayload,
|
||||||
ProfitInterface,
|
ProfitInterface,
|
||||||
Trade,
|
Trade,
|
||||||
|
TimeSummaryOptions,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { createBotSubStore } from './ftbot';
|
import { createBotSubStore } from './ftbot';
|
||||||
|
@ -121,9 +122,9 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
allDailyStatsSelectedBots: (state): DailyReturnValue => {
|
allDailyStatsSelectedBots: (state): TimeSummaryReturnValue => {
|
||||||
// Return aggregated daily stats for all bots - sorted ascending.
|
// Return aggregated daily stats for all bots - sorted ascending.
|
||||||
const resp: Record<string, DailyRecord> = {};
|
const resp: Record<string, TimeSummaryRecord> = {};
|
||||||
Object.entries(state.botStores).forEach(([, botStore]) => {
|
Object.entries(state.botStores).forEach(([, botStore]) => {
|
||||||
if (botStore.isSelected) {
|
if (botStore.isSelected) {
|
||||||
botStore.dailyStats?.data?.forEach((d) => {
|
botStore.dailyStats?.data?.forEach((d) => {
|
||||||
|
@ -138,7 +139,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const dailyReturn: DailyReturnValue = {
|
const dailyReturn: TimeSummaryReturnValue = {
|
||||||
stake_currency: 'USDT',
|
stake_currency: 'USDT',
|
||||||
fiat_display_currency: 'USD',
|
fiat_display_currency: 'USD',
|
||||||
data: Object.values(resp).sort((a, b) => (a.date > b.date ? 1 : -1)),
|
data: Object.values(resp).sort((a, b) => (a.date > b.date ? 1 : -1)),
|
||||||
|
@ -247,7 +248,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
||||||
const updates: Promise<void>[] = [];
|
const updates: Promise<void>[] = [];
|
||||||
updates.push(this.allRefreshFrequent(false));
|
updates.push(this.allRefreshFrequent(false));
|
||||||
updates.push(this.allRefreshSlow(true));
|
updates.push(this.allRefreshSlow(true));
|
||||||
// updates.push(this.getDaily());
|
// updates.push(this.getTimeSummary());
|
||||||
// updates.push(this.getBalance());
|
// updates.push(this.getBalance());
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
console.log('refreshing_end');
|
console.log('refreshing_end');
|
||||||
|
@ -303,12 +304,12 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async allGetDaily(payload: DailyPayload) {
|
async allGetDaily(payload: TimeSummaryPayload) {
|
||||||
const updates: Promise<DailyReturnValue>[] = [];
|
const updates: Promise<TimeSummaryReturnValue>[] = [];
|
||||||
|
|
||||||
this.allBotStores.forEach((bot) => {
|
this.allBotStores.forEach((bot) => {
|
||||||
if (bot.isBotOnline) {
|
if (bot.isBotOnline) {
|
||||||
updates.push(bot.getDaily(payload));
|
updates.push(bot.getTimeSummary(TimeSummaryOptions.daily, payload));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
|
|
|
@ -80,6 +80,12 @@
|
||||||
background: $bg-dark;
|
background: $bg-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group-item.disabled,
|
||||||
|
.list-group-item:disabled {
|
||||||
|
color: darken($fg-color, 40%);
|
||||||
|
background: $bg-dark;
|
||||||
|
}
|
||||||
|
|
||||||
// .custom-select {
|
// .custom-select {
|
||||||
// color: $fg-color;
|
// color: $fg-color;
|
||||||
// // background: $bg-dark;
|
// // background: $bg-dark;
|
||||||
|
@ -214,13 +220,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.b-toast .toast {
|
.b-toast .toast {
|
||||||
background: $bg-dark;
|
color: black;
|
||||||
|
// background: $bg-dark;
|
||||||
border-color: lighten($bg-dark, 20%);
|
border-color: lighten($bg-dark, 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-header {
|
.toast-header {
|
||||||
color: $fg-color;
|
color: $fg-color;
|
||||||
background: darken($bg-dark, 10%);
|
// background: darken($bg-dark, 10%);
|
||||||
border-color: lighten($bg-dark, 20%);
|
border-color: lighten($bg-dark, 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ export interface PairResult {
|
||||||
profit_total: number;
|
profit_total: number;
|
||||||
trades: number;
|
trades: number;
|
||||||
wins: number;
|
wins: number;
|
||||||
|
winrate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExitReasonResults {
|
export interface ExitReasonResults {
|
||||||
|
@ -48,6 +49,7 @@ export interface ExitReasonResults {
|
||||||
profit_total_pct: number;
|
profit_total_pct: number;
|
||||||
trades: number;
|
trades: number;
|
||||||
wins: number;
|
wins: number;
|
||||||
|
winrate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
|
@ -59,6 +61,7 @@ export interface PeriodicStat {
|
||||||
wins: number;
|
wins: number;
|
||||||
draws: number;
|
draws: number;
|
||||||
loses: number;
|
loses: number;
|
||||||
|
winrate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeriodicBreakdown {
|
export interface PeriodicBreakdown {
|
||||||
|
@ -137,7 +140,6 @@ export interface StrategyBacktestResult {
|
||||||
canceled_entry_orders?: number;
|
canceled_entry_orders?: number;
|
||||||
replaced_entry_orders?: number;
|
replaced_entry_orders?: number;
|
||||||
|
|
||||||
// Daily stats ...
|
|
||||||
draw_days: number;
|
draw_days: number;
|
||||||
drawdown_end: string;
|
drawdown_end: string;
|
||||||
drawdown_end_ts: number;
|
drawdown_end_ts: number;
|
||||||
|
@ -145,6 +147,8 @@ export interface StrategyBacktestResult {
|
||||||
drawdown_start_ts: number;
|
drawdown_start_ts: number;
|
||||||
loser_holding_avg: string;
|
loser_holding_avg: string;
|
||||||
loser_holding_avg_s: number;
|
loser_holding_avg_s: number;
|
||||||
|
max_consecutive_wins?: number;
|
||||||
|
max_consecutive_losses?: number;
|
||||||
losing_days: number;
|
losing_days: number;
|
||||||
max_drawdown: number;
|
max_drawdown: number;
|
||||||
max_drawdown_account: number;
|
max_drawdown_account: number;
|
||||||
|
@ -159,6 +163,7 @@ export interface StrategyBacktestResult {
|
||||||
sharpe?: number;
|
sharpe?: number;
|
||||||
calmar?: number;
|
calmar?: number;
|
||||||
expectancy?: number;
|
expectancy?: number;
|
||||||
|
expectancy_ratio?: number;
|
||||||
|
|
||||||
winner_holding_avg: string;
|
winner_holding_avg: string;
|
||||||
winner_holding_avg_s: number;
|
winner_holding_avg_s: number;
|
||||||
|
@ -177,9 +182,42 @@ export interface StrategyBacktestResult {
|
||||||
backtest_run_end_ts: number;
|
backtest_run_end_ts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BacktestMetadata {
|
||||||
|
/** Start time of the backtest run */
|
||||||
|
backtest_run_start_ts: number;
|
||||||
|
run_id: string;
|
||||||
|
filename?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only used in memory */
|
||||||
|
export interface BacktestMetadataWithStrategyName extends BacktestMetadata {
|
||||||
|
strategyName: string;
|
||||||
|
editing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestMetadataPatch {
|
||||||
|
notes: string;
|
||||||
|
strategy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestResultUpdate extends BacktestMetadataPatch {
|
||||||
|
run_id: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the in-memory result of a backtest.
|
||||||
|
*/
|
||||||
|
export interface BacktestResultInMemory {
|
||||||
|
strategy: StrategyBacktestResult;
|
||||||
|
metadata: BacktestMetadataWithStrategyName;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BacktestResult {
|
export interface BacktestResult {
|
||||||
strategy: Record<string, StrategyBacktestResult>;
|
strategy: Record<string, StrategyBacktestResult>;
|
||||||
strategy_comparison: Array<Record<string, string | number>>;
|
strategy_comparison: Array<Record<string, string | number>>;
|
||||||
|
metadata: Record<string, BacktestMetadata>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BacktestSteps {
|
export enum BacktestSteps {
|
||||||
|
@ -206,4 +244,5 @@ export interface BacktestHistoryEntry {
|
||||||
strategy: string;
|
strategy: string;
|
||||||
run_id: string;
|
run_id: string;
|
||||||
backtest_start_time: number;
|
backtest_start_time: number;
|
||||||
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,5 @@ export interface ComparisonTableItems {
|
||||||
losses: number;
|
losses: number;
|
||||||
balance?: number;
|
balance?: number;
|
||||||
stakeCurrencyDecimals?: number;
|
stakeCurrencyDecimals?: number;
|
||||||
|
isDryRun?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
export interface DailyPayload {
|
export enum TimeSummaryOptions {
|
||||||
|
daily = 'daily',
|
||||||
|
weekly = 'weekly',
|
||||||
|
monthly = 'monthly',
|
||||||
|
}
|
||||||
|
export interface TimeSummaryPayload {
|
||||||
timescale?: number;
|
timescale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DailyRecord {
|
export interface TimeSummaryRecord {
|
||||||
/** Date in the format yyyy-mm-dd */
|
/** Date in the format yyyy-mm-dd */
|
||||||
[key: string]: string | number;
|
[key: string]: string | number;
|
||||||
date: string;
|
date: string;
|
||||||
|
@ -14,8 +19,8 @@ export interface DailyRecord {
|
||||||
trade_count: number;
|
trade_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DailyReturnValue {
|
export interface TimeSummaryReturnValue {
|
||||||
data: DailyRecord[];
|
data: TimeSummaryRecord[];
|
||||||
fiat_display_currency: string;
|
fiat_display_currency: string;
|
||||||
stake_currency: string;
|
stake_currency: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,15 @@ export interface ProfitInterface {
|
||||||
profit_factor?: number;
|
profit_factor?: number;
|
||||||
max_drawdown?: number;
|
max_drawdown?: number;
|
||||||
max_drawdown_abs?: number;
|
max_drawdown_abs?: number;
|
||||||
|
max_drawdown_start?: string;
|
||||||
|
max_drawdown_start_timestamp?: number;
|
||||||
|
max_drawdown_end?: string;
|
||||||
|
max_drawdown_end_timestamp?: number;
|
||||||
trading_volume?: number;
|
trading_volume?: number;
|
||||||
/** Initial bot start date*/
|
/** Initial bot start date*/
|
||||||
bot_start_timestamp?: number;
|
bot_start_timestamp?: number;
|
||||||
bot_start_date?: string;
|
bot_start_date?: string;
|
||||||
|
winrate?: number;
|
||||||
|
expectancy?: number;
|
||||||
|
expectancy_ratio?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,10 @@ interface TradeBase {
|
||||||
initial_stop_loss_ratio?: number;
|
initial_stop_loss_ratio?: number;
|
||||||
initial_stop_loss_pct?: number;
|
initial_stop_loss_pct?: number;
|
||||||
|
|
||||||
|
/** deprecated, to be replaced with "has_open_orders" */
|
||||||
open_order_id?: string;
|
open_order_id?: string;
|
||||||
|
/** Added only recently, replaces open_order_id */
|
||||||
|
has_open_orders?: boolean;
|
||||||
/** Short properties - only available in API versions 2.x and up */
|
/** Short properties - only available in API versions 2.x and up */
|
||||||
is_short?: boolean;
|
is_short?: boolean;
|
||||||
leverage?: number;
|
leverage?: number;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container-fluid" style="max-height: calc(100vh - 60px)">
|
<div class="d-flex flex-column pt-1 me-1" style="height: calc(100vh - 60px)">
|
||||||
<div class="container-fluid">
|
<div>
|
||||||
<div class="row mb-2"></div>
|
<div class="d-flex flex-row">
|
||||||
|
<h2 class="ms-5">Backtesting</h2>
|
||||||
<p v-if="!botStore.activeBot.canRunBacktest">
|
<p v-if="!botStore.activeBot.canRunBacktest">
|
||||||
Bot must be in webserver mode to enable Backtesting.
|
Bot must be in webserver mode to enable Backtesting.
|
||||||
</p>
|
</p>
|
||||||
<div class="row w-100">
|
<div class="w-100">
|
||||||
<h2 class="col-4 col-lg-3">Backtesting</h2>
|
|
||||||
<div
|
<div
|
||||||
class="col-12 col-lg-order-last col-lg-6 mx-md-5 d-flex flex-wrap justify-content-md-center justify-content-between mb-4"
|
class="mx-md-5 d-flex flex-wrap justify-content-md-center justify-content-between mb-4 gap-2"
|
||||||
>
|
>
|
||||||
<b-form-radio
|
<b-form-radio
|
||||||
v-if="botStore.activeBot.botApiVersion >= 2.15"
|
v-if="botStore.activeBot.botApiVersion >= 2.15"
|
||||||
|
@ -58,22 +58,22 @@
|
||||||
>Visualize result</b-form-radio
|
>Visualize result</b-form-radio
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<small
|
<small v-show="botStore.activeBot.backtestRunning" class="text-end bt-running-label"
|
||||||
v-show="botStore.activeBot.backtestRunning"
|
|
||||||
class="text-end bt-running-label col-8 col-lg-3"
|
|
||||||
>Backtest running: {{ botStore.activeBot.backtestStep }}
|
>Backtest running: {{ botStore.activeBot.backtestStep }}
|
||||||
{{ formatPercent(botStore.activeBot.backtestProgress, 2) }}</small
|
{{ formatPercent(botStore.activeBot.backtestProgress, 2) }}</small
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="d-md-flex">
|
<div class="d-flex flex-md-row">
|
||||||
<!-- Left bar -->
|
<!-- Left bar -->
|
||||||
<div
|
<div
|
||||||
:class="`${showLeftBar ? 'col-md-3' : ''} sticky-top sticky-offset me-3 d-flex flex-column`"
|
v-if="btFormMode !== 'visualize'"
|
||||||
|
:class="`${showLeftBar ? 'col-md-3' : ''}`"
|
||||||
|
class="sticky-top sticky-offset me-3 d-flex flex-column absolute"
|
||||||
|
style="max-height: calc(100vh - 60px)"
|
||||||
>
|
>
|
||||||
<b-button
|
<b-button
|
||||||
v-if="btFormMode !== 'visualize'"
|
|
||||||
class="align-self-start"
|
class="align-self-start"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -85,224 +85,35 @@
|
||||||
v-if="btFormMode !== 'visualize' && showLeftBar"
|
v-if="btFormMode !== 'visualize' && showLeftBar"
|
||||||
:backtest-history="botStore.activeBot.backtestHistory"
|
:backtest-history="botStore.activeBot.backtestHistory"
|
||||||
:selected-backtest-result-key="botStore.activeBot.selectedBacktestResultKey"
|
:selected-backtest-result-key="botStore.activeBot.selectedBacktestResultKey"
|
||||||
|
:can-use-modify="botStore.activeBot.botApiVersion >= 2.32"
|
||||||
@selection-change="botStore.activeBot.setBacktestResultKey"
|
@selection-change="botStore.activeBot.setBacktestResultKey"
|
||||||
|
@remove-result="botStore.activeBot.removeBacktestResultFromMemory"
|
||||||
|
@update-result="botStore.activeBot.saveBacktestResultMetadata"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<!-- End Left bar -->
|
<!-- End Left bar -->
|
||||||
|
<div class="d-flex flex-column flex-fill mw-100">
|
||||||
|
<div class="d-md-flex">
|
||||||
<div
|
<div
|
||||||
v-if="btFormMode == 'historicResults'"
|
v-if="btFormMode == 'historicResults'"
|
||||||
class="flex-fill row d-flex flex-column bt-config"
|
class="flex-fill d-flex flex-column bt-config"
|
||||||
>
|
>
|
||||||
<backtest-history-load />
|
<BacktestHistoryLoad />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="btFormMode == 'run'" class="flex-fill row d-flex flex-column bt-config">
|
<div v-if="btFormMode == 'run'" class="flex-fill d-flex flex-column bt-config">
|
||||||
<div class="mb-2">
|
<BacktestRun />
|
||||||
<span>Strategy</span>
|
|
||||||
<StrategySelect v-model="strategy"></StrategySelect>
|
|
||||||
</div>
|
</div>
|
||||||
<b-card :disabled="botStore.activeBot.backtestRunning">
|
<BacktestResultAnalysis
|
||||||
<!-- Backtesting parameters -->
|
|
||||||
<b-form-group
|
|
||||||
label-cols-lg="2"
|
|
||||||
label="Backtest params"
|
|
||||||
label-size="sm"
|
|
||||||
label-class="fw-bold pt-0"
|
|
||||||
class="mb-0"
|
|
||||||
>
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Timeframe:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="timeframe-select"
|
|
||||||
>
|
|
||||||
<TimeframeSelect id="timeframe-select" v-model="selectedTimeframe" />
|
|
||||||
</b-form-group>
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Detail Timeframe:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="timeframe-detail-select"
|
|
||||||
title="Detail timeframe, to simulate intra-candle results. Not setting this will not use this functionality."
|
|
||||||
>
|
|
||||||
<TimeframeSelect
|
|
||||||
id="timeframe-detail-select"
|
|
||||||
v-model="selectedDetailTimeframe"
|
|
||||||
:below-timeframe="selectedTimeframe"
|
|
||||||
/>
|
|
||||||
</b-form-group>
|
|
||||||
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Max open trades:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="max-open-trades"
|
|
||||||
>
|
|
||||||
<b-form-input
|
|
||||||
id="max-open-trades"
|
|
||||||
v-model="maxOpenTrades"
|
|
||||||
placeholder="Use strategy default"
|
|
||||||
type="number"
|
|
||||||
></b-form-input>
|
|
||||||
</b-form-group>
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Starting capital:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="starting-capital"
|
|
||||||
>
|
|
||||||
<b-form-input
|
|
||||||
id="starting-capital"
|
|
||||||
v-model="startingCapital"
|
|
||||||
type="number"
|
|
||||||
step="0.001"
|
|
||||||
></b-form-input>
|
|
||||||
</b-form-group>
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Stake amount:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="stake-amount"
|
|
||||||
>
|
|
||||||
<div class="d-flex">
|
|
||||||
<b-form-checkbox
|
|
||||||
id="stake-amount-bool"
|
|
||||||
v-model="stakeAmountUnlimited"
|
|
||||||
class="col-md-6"
|
|
||||||
>Unlimited stake</b-form-checkbox
|
|
||||||
>
|
|
||||||
|
|
||||||
<b-form-input
|
|
||||||
id="stake-amount"
|
|
||||||
v-model="stakeAmount"
|
|
||||||
type="number"
|
|
||||||
placeholder="Use strategy default"
|
|
||||||
step="0.01"
|
|
||||||
:disabled="stakeAmountUnlimited"
|
|
||||||
></b-form-input>
|
|
||||||
</div>
|
|
||||||
</b-form-group>
|
|
||||||
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Enable Protections:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="enable-protections"
|
|
||||||
>
|
|
||||||
<b-form-checkbox
|
|
||||||
id="enable-protections"
|
|
||||||
v-model="enableProtections"
|
|
||||||
></b-form-checkbox>
|
|
||||||
</b-form-group>
|
|
||||||
<b-form-group
|
|
||||||
v-if="botStore.activeBot.botApiVersion >= 2.22"
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Cache Backtest results:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="enable-cache"
|
|
||||||
>
|
|
||||||
<b-form-checkbox id="enable-cache" v-model="allowCache"></b-form-checkbox>
|
|
||||||
</b-form-group>
|
|
||||||
<template v-if="botStore.activeBot.botApiVersion >= 2.22">
|
|
||||||
<b-form-group
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="Enable FreqAI:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="enable-freqai"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<span class="me-2">Enable FreqAI:</span>
|
|
||||||
<InfoBox
|
|
||||||
hint="Assumes freqAI configuration is setup in the configuration, and the strategy is a freqAI strategy. Will fail if that's not the case."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<b-form-checkbox id="enable-freqai" v-model="freqAI.enabled"></b-form-checkbox>
|
|
||||||
</b-form-group>
|
|
||||||
<b-form-group
|
|
||||||
v-if="freqAI.enabled"
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="FreqAI identifier:"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="freqai-identifier"
|
|
||||||
>
|
|
||||||
<b-form-input
|
|
||||||
id="freqai-identifier"
|
|
||||||
v-model="freqAI.identifier"
|
|
||||||
placeholder="Use config default"
|
|
||||||
></b-form-input>
|
|
||||||
</b-form-group>
|
|
||||||
<b-form-group
|
|
||||||
v-if="freqAI.enabled"
|
|
||||||
label-cols-sm="5"
|
|
||||||
label="FreqAI Model"
|
|
||||||
label-align-sm="right"
|
|
||||||
label-for="freqai-model"
|
|
||||||
>
|
|
||||||
<FreqaiModelSelect id="freqai-model" v-model="freqAI.model"></FreqaiModelSelect>
|
|
||||||
</b-form-group>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- <b-form-group label-cols-sm="5" label="Fee:" label-align-sm="right" label-for="fee">
|
|
||||||
<b-form-input
|
|
||||||
id="fee"
|
|
||||||
type="number"
|
|
||||||
placeholder="Use exchange default"
|
|
||||||
step="0.01"
|
|
||||||
></b-form-input>
|
|
||||||
</b-form-group> -->
|
|
||||||
<hr />
|
|
||||||
<TimeRangeSelect v-model="timerange" class="mt-2"></TimeRangeSelect>
|
|
||||||
</b-form-group>
|
|
||||||
</b-card>
|
|
||||||
|
|
||||||
<h3 class="mt-3">Backtesting summary</h3>
|
|
||||||
<div
|
|
||||||
class="d-flex flex-wrap flex-md-nowrap justify-content-between justify-content-md-center"
|
|
||||||
>
|
|
||||||
<b-button
|
|
||||||
id="start-backtest"
|
|
||||||
variant="primary"
|
|
||||||
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
|
||||||
class="mx-1"
|
|
||||||
@click="clickBacktest"
|
|
||||||
>
|
|
||||||
Start backtest
|
|
||||||
</b-button>
|
|
||||||
<b-button
|
|
||||||
variant="primary"
|
|
||||||
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
|
||||||
class="mx-1"
|
|
||||||
@click="botStore.activeBot.pollBacktest"
|
|
||||||
>
|
|
||||||
Load backtest result
|
|
||||||
</b-button>
|
|
||||||
<b-button
|
|
||||||
variant="primary"
|
|
||||||
class="mx-1"
|
|
||||||
:disabled="!botStore.activeBot.backtestRunning"
|
|
||||||
@click="botStore.activeBot.stopBacktest"
|
|
||||||
>Stop Backtest</b-button
|
|
||||||
>
|
|
||||||
<b-button
|
|
||||||
variant="primary"
|
|
||||||
class="mx-1"
|
|
||||||
:disabled="botStore.activeBot.backtestRunning || !botStore.activeBot.canRunBacktest"
|
|
||||||
@click="botStore.activeBot.removeBacktest"
|
|
||||||
>Reset Backtest</b-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<BacktestResultView
|
|
||||||
v-if="hasBacktestResult && btFormMode == 'results'"
|
v-if="hasBacktestResult && btFormMode == 'results'"
|
||||||
:backtest-result="botStore.activeBot.selectedBacktestResult"
|
:backtest-result="botStore.activeBot.selectedBacktestResult"
|
||||||
class="flex-fill"
|
class="flex-fill"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BacktestGraphsView
|
<BacktestGraphs
|
||||||
v-if="hasBacktestResult && btFormMode == 'visualize-summary'"
|
v-if="hasBacktestResult && btFormMode == 'visualize-summary'"
|
||||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||||
|
class="flex-fill"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -312,35 +123,33 @@
|
||||||
>
|
>
|
||||||
<BacktestResultChart
|
<BacktestResultChart
|
||||||
:timeframe="timeframe"
|
:timeframe="timeframe"
|
||||||
:strategy="strategy"
|
:strategy="btStore.strategy"
|
||||||
:timerange="timerange"
|
:timerange="btStore.timerange"
|
||||||
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
|
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
|
||||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||||
:freqai-model="freqAI.enabled ? freqAI.model : undefined"
|
:freqai-model="btStore.freqAI.enabled ? btStore.freqAI.model : undefined"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TimeRangeSelect from '@/components/ftbot/TimeRangeSelect.vue';
|
import BacktestGraphs from '@/components/ftbot/BacktestGraphs.vue';
|
||||||
import BacktestResultView from '@/components/ftbot/BacktestResultView.vue';
|
|
||||||
import BacktestResultSelect from '@/components/ftbot/BacktestResultSelect.vue';
|
|
||||||
import StrategySelect from '@/components/ftbot/StrategySelect.vue';
|
|
||||||
import FreqaiModelSelect from '@/components/ftbot/FreqaiModelSelect.vue';
|
|
||||||
import TimeframeSelect from '@/components/ftbot/TimeframeSelect.vue';
|
|
||||||
import BacktestHistoryLoad from '@/components/ftbot/BacktestHistoryLoad.vue';
|
import BacktestHistoryLoad from '@/components/ftbot/BacktestHistoryLoad.vue';
|
||||||
import BacktestGraphsView from '@/components/ftbot/BacktestGraphsView.vue';
|
|
||||||
import BacktestResultChart from '@/components/ftbot/BacktestResultChart.vue';
|
import BacktestResultChart from '@/components/ftbot/BacktestResultChart.vue';
|
||||||
import InfoBox from '@/components/general/InfoBox.vue';
|
import BacktestResultSelect from '@/components/ftbot/BacktestResultSelect.vue';
|
||||||
|
import BacktestResultAnalysis from '@/components/ftbot/BacktestResultAnalysis.vue';
|
||||||
import { BacktestPayload } from '@/types';
|
import BacktestRun from '@/components/ftbot/BacktestRun.vue';
|
||||||
|
|
||||||
import { formatPercent } from '@/shared/formatters';
|
import { formatPercent } from '@/shared/formatters';
|
||||||
import { computed, ref, onMounted, watch } from 'vue';
|
import { useBtStore } from '@/stores/btStore';
|
||||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const botStore = useBotStore();
|
const botStore = useBotStore();
|
||||||
|
const btStore = useBtStore();
|
||||||
|
|
||||||
const hasBacktestResult = computed(() =>
|
const hasBacktestResult = computed(() =>
|
||||||
botStore.activeBot.backtestHistory
|
botStore.activeBot.backtestHistory
|
||||||
|
@ -355,33 +164,20 @@ const timeframe = computed((): string => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const strategy = ref('');
|
|
||||||
const selectedTimeframe = ref('');
|
|
||||||
const selectedDetailTimeframe = ref('');
|
|
||||||
const timerange = ref('');
|
|
||||||
const showLeftBar = ref(false);
|
const showLeftBar = ref(false);
|
||||||
const freqAI = ref({
|
|
||||||
enabled: false,
|
|
||||||
model: '',
|
|
||||||
identifier: '',
|
|
||||||
});
|
|
||||||
const enableProtections = ref(false);
|
|
||||||
const stakeAmountUnlimited = ref(false);
|
|
||||||
const allowCache = ref(true);
|
|
||||||
const maxOpenTrades = ref('');
|
|
||||||
const stakeAmount = ref('');
|
|
||||||
const startingCapital = ref('');
|
|
||||||
const btFormMode = ref('run');
|
const btFormMode = ref('run');
|
||||||
const pollInterval = ref<number | null>(null);
|
const pollInterval = ref<number | null>(null);
|
||||||
|
|
||||||
const selectBacktestResult = () => {
|
const selectBacktestResult = () => {
|
||||||
// Set parameters for this result
|
// Set parameters for this result
|
||||||
strategy.value = botStore.activeBot.selectedBacktestResult.strategy_name;
|
btStore.strategy = botStore.activeBot.selectedBacktestResult.strategy_name;
|
||||||
botStore.activeBot.getStrategy(strategy.value);
|
botStore.activeBot.getStrategy(btStore.strategy);
|
||||||
selectedTimeframe.value = botStore.activeBot.selectedBacktestResult.timeframe;
|
btStore.selectedTimeframe = botStore.activeBot.selectedBacktestResult.timeframe;
|
||||||
selectedDetailTimeframe.value = botStore.activeBot.selectedBacktestResult.timeframe_detail || '';
|
btStore.selectedDetailTimeframe =
|
||||||
|
botStore.activeBot.selectedBacktestResult.timeframe_detail || '';
|
||||||
// TODO: maybe this should not use timerange, but the actual backtest start/end results instead?
|
// TODO: maybe this should not use timerange, but the actual backtest start/end results instead?
|
||||||
timerange.value = botStore.activeBot.selectedBacktestResult.timerange;
|
btStore.timerange = botStore.activeBot.selectedBacktestResult.timerange;
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
@ -391,49 +187,6 @@ watch(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const clickBacktest = () => {
|
|
||||||
const btPayload: BacktestPayload = {
|
|
||||||
strategy: strategy.value,
|
|
||||||
timerange: timerange.value,
|
|
||||||
enable_protections: enableProtections.value,
|
|
||||||
};
|
|
||||||
const openTradesInt = parseInt(maxOpenTrades.value, 10);
|
|
||||||
if (openTradesInt) {
|
|
||||||
btPayload.max_open_trades = openTradesInt;
|
|
||||||
}
|
|
||||||
if (stakeAmountUnlimited.value) {
|
|
||||||
btPayload.stake_amount = 'unlimited';
|
|
||||||
} else {
|
|
||||||
const stakeAmountLoc = Number(stakeAmount.value);
|
|
||||||
if (stakeAmountLoc) {
|
|
||||||
btPayload.stake_amount = stakeAmountLoc.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startingCapitalLoc = Number(startingCapital.value);
|
|
||||||
if (startingCapitalLoc) {
|
|
||||||
btPayload.dry_run_wallet = startingCapitalLoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTimeframe.value) {
|
|
||||||
btPayload.timeframe = selectedTimeframe.value;
|
|
||||||
}
|
|
||||||
if (selectedDetailTimeframe.value) {
|
|
||||||
btPayload.timeframe_detail = selectedDetailTimeframe.value;
|
|
||||||
}
|
|
||||||
if (!allowCache.value) {
|
|
||||||
btPayload.backtest_cache = 'none';
|
|
||||||
}
|
|
||||||
if (freqAI.value.enabled) {
|
|
||||||
btPayload.freqaimodel = freqAI.value.model;
|
|
||||||
if (freqAI.value.identifier !== '') {
|
|
||||||
btPayload.freqai = { identifier: freqAI.value.identifier };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
botStore.activeBot.startBacktest(btPayload);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => botStore.activeBot.getState());
|
onMounted(() => botStore.activeBot.getState());
|
||||||
watch(
|
watch(
|
||||||
() => botStore.activeBot.backtestRunning,
|
() => botStore.activeBot.backtestRunning,
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||||
:col-num="12"
|
:col-num="12"
|
||||||
@layout-updated="layoutUpdatedEvent"
|
@layout-updated="layoutUpdatedEvent"
|
||||||
@breakpoint-changed="breakpointChanged"
|
@update:breakpoint="breakpointChanged"
|
||||||
>
|
>
|
||||||
<template #default="{ gridItemProps }">
|
<template #default="{ gridItemProps }">
|
||||||
<grid-item
|
<grid-item
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
drag-allow-from=".drag-header"
|
drag-allow-from=".drag-header"
|
||||||
>
|
>
|
||||||
<DraggableContainer :header="`Daily Profit ${botStore.botCount > 1 ? 'combined' : ''}`">
|
<DraggableContainer :header="`Daily Profit ${botStore.botCount > 1 ? 'combined' : ''}`">
|
||||||
<DailyChart
|
<TimePeriodChart
|
||||||
v-if="botStore.allDailyStatsSelectedBots"
|
v-if="botStore.allDailyStatsSelectedBots"
|
||||||
:daily-stats="botStore.allDailyStatsSelectedBots"
|
:daily-stats="botStore.allDailyStatsSelectedBots"
|
||||||
:show-title="false"
|
:show-title="false"
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
|
||||||
import DailyChart from '@/components/charts/DailyChart.vue';
|
import TimePeriodChart from '@/components/charts/TimePeriodChart.vue';
|
||||||
import CumProfitChart from '@/components/charts/CumProfitChart.vue';
|
import CumProfitChart from '@/components/charts/CumProfitChart.vue';
|
||||||
import TradesLogChart from '@/components/charts/TradesLog.vue';
|
import TradesLogChart from '@/components/charts/TradesLog.vue';
|
||||||
import ProfitDistributionChart from '@/components/charts/ProfitDistributionChart.vue';
|
import ProfitDistributionChart from '@/components/charts/ProfitDistributionChart.vue';
|
||||||
|
@ -177,8 +177,8 @@ const botStore = useBotStore();
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const currentBreakpoint = ref('');
|
const currentBreakpoint = ref('');
|
||||||
|
|
||||||
const breakpointChanged = (newBreakpoint) => {
|
const breakpointChanged = (newBreakpoint: string) => {
|
||||||
// // console.log('breakpoint:', newBreakpoint);
|
// console.log('breakpoint:', newBreakpoint);
|
||||||
currentBreakpoint.value = newBreakpoint;
|
currentBreakpoint.value = newBreakpoint;
|
||||||
};
|
};
|
||||||
const isResizableLayout = computed(() =>
|
const isResizableLayout = computed(() =>
|
||||||
|
@ -235,7 +235,7 @@ const responsiveGridLayouts = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await botStore.allGetDaily({ timescale: 30 });
|
botStore.allGetDaily({ timescale: 30 });
|
||||||
// botStore.activeBot.getTrades();
|
// botStore.activeBot.getTrades();
|
||||||
botStore.activeBot.getOpenTrades();
|
botStore.activeBot.getOpenTrades();
|
||||||
botStore.activeBot.getProfit();
|
botStore.activeBot.getProfit();
|
||||||
|
|
|
@ -45,12 +45,12 @@
|
||||||
<b-tab title="Balance" lazy>
|
<b-tab title="Balance" lazy>
|
||||||
<Balance />
|
<Balance />
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<b-tab title="Daily Stats" lazy>
|
<b-tab title="Time Breakdown" lazy>
|
||||||
<DailyStats />
|
<PeriodBreakdown />
|
||||||
</b-tab>
|
</b-tab>
|
||||||
|
|
||||||
<b-tab title="Pairlist" lazy>
|
<b-tab title="Pairlist" lazy>
|
||||||
<FTBotAPIPairList />
|
<PairListLive />
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<b-tab title="Pair Locks" lazy>
|
<b-tab title="Pair Locks" lazy>
|
||||||
<PairLockList />
|
<PairLockList />
|
||||||
|
@ -152,9 +152,9 @@ import Balance from '@/components/ftbot/BotBalance.vue';
|
||||||
import BotControls from '@/components/ftbot/BotControls.vue';
|
import BotControls from '@/components/ftbot/BotControls.vue';
|
||||||
import BotStatus from '@/components/ftbot/BotStatus.vue';
|
import BotStatus from '@/components/ftbot/BotStatus.vue';
|
||||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||||
import DailyStats from '@/components/ftbot/DailyStats.vue';
|
import PeriodBreakdown from '@/components/ftbot/PeriodBreakdown.vue';
|
||||||
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
||||||
import FTBotAPIPairList from '@/components/ftbot/FTBotAPIPairList.vue';
|
import PairListLive from '@/components/ftbot/PairListLive.vue';
|
||||||
import PairLockList from '@/components/ftbot/PairLockList.vue';
|
import PairLockList from '@/components/ftbot/PairLockList.vue';
|
||||||
import PairSummary from '@/components/ftbot/PairSummary.vue';
|
import PairSummary from '@/components/ftbot/PairSummary.vue';
|
||||||
import BotPerformance from '@/components/ftbot/BotPerformance.vue';
|
import BotPerformance from '@/components/ftbot/BotPerformance.vue';
|
||||||
|
@ -169,7 +169,7 @@ const botStore = useBotStore();
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const currentBreakpoint = ref('');
|
const currentBreakpoint = ref('');
|
||||||
|
|
||||||
const breakpointChanged = (newBreakpoint) => {
|
const breakpointChanged = (newBreakpoint: string) => {
|
||||||
// console.log('breakpoint:', newBreakpoint);
|
// console.log('breakpoint:', newBreakpoint);
|
||||||
currentBreakpoint.value = newBreakpoint;
|
currentBreakpoint.value = newBreakpoint;
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
dateStringToTimeRange,
|
dateStringToTimeRange,
|
||||||
timestampHour,
|
timestampHour,
|
||||||
dateFromString,
|
dateFromString,
|
||||||
|
timestampmsOrNa,
|
||||||
} from '@/shared/formatters';
|
} from '@/shared/formatters';
|
||||||
|
|
||||||
const { getTimeZone } = exportForTesting;
|
const { getTimeZone } = exportForTesting;
|
||||||
|
@ -31,6 +32,8 @@ describe('timeformatter.ts', () => {
|
||||||
expect(timestampmsWithTimezone(1651057500000)).toEqual('2022-04-27 11:05:00 (UTC)');
|
expect(timestampmsWithTimezone(1651057500000)).toEqual('2022-04-27 11:05:00 (UTC)');
|
||||||
setTimezone('UTC');
|
setTimezone('UTC');
|
||||||
expect(timestampmsWithTimezone(1651057500000)).toEqual('2022-04-27 11:05:00 (UTC)');
|
expect(timestampmsWithTimezone(1651057500000)).toEqual('2022-04-27 11:05:00 (UTC)');
|
||||||
|
expect(timestampmsWithTimezone(0)).toEqual('N/A');
|
||||||
|
expect(timestampmsWithTimezone(null)).toEqual('N/A');
|
||||||
});
|
});
|
||||||
it('timestampms convert correctly', () => {
|
it('timestampms convert correctly', () => {
|
||||||
setTimezone('UTC');
|
setTimezone('UTC');
|
||||||
|
@ -38,6 +41,16 @@ describe('timeformatter.ts', () => {
|
||||||
setTimezone('CET');
|
setTimezone('CET');
|
||||||
expect(timestampms(1651057500000)).toEqual('2022-04-27 13:05:00');
|
expect(timestampms(1651057500000)).toEqual('2022-04-27 13:05:00');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('timestampmsOrNA convert correctly', () => {
|
||||||
|
setTimezone('UTC');
|
||||||
|
expect(timestampmsOrNa(1651057500000)).toEqual('2022-04-27 11:05:00');
|
||||||
|
setTimezone('CET');
|
||||||
|
expect(timestampmsOrNa(1651057500000)).toEqual('2022-04-27 13:05:00');
|
||||||
|
|
||||||
|
expect(timestampmsOrNa(0)).toEqual('N/A');
|
||||||
|
expect(timestampmsOrNa(null)).toEqual('N/A');
|
||||||
|
});
|
||||||
it('timestampToDateString converts to date', () => {
|
it('timestampToDateString converts to date', () => {
|
||||||
expect(timestampToDateString(1651057500000)).toEqual('2022-04-27');
|
expect(timestampToDateString(1651057500000)).toEqual('2022-04-27');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user