mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-23 11:35:14 +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
|
||||
'no-shadow': 'off',
|
||||
'prettier/prettier': ['error'],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
// '@typescript-eslint/naming-convention': [
|
||||
// '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**
|
||||
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"
|
||||
time: "03:00"
|
||||
timezone: "UTC"
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 20
|
||||
target-branch: main
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
|
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -21,10 +21,10 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-22.04 ]
|
||||
node: [ "16", "18", "19", "20"]
|
||||
node: [ "16", "18", "20"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
run: yarn test:unit
|
||||
|
||||
- name: Run Component tests
|
||||
uses: cypress-io/github-action@v5
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
# we have already installed everything
|
||||
install: false
|
||||
|
@ -50,7 +50,7 @@ jobs:
|
|||
command: yarn cypress run --component
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v5
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
# build: yarn build
|
||||
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
|
||||
|
||||
|
@ -19,7 +19,7 @@ COPY . /app
|
|||
# webpack and node17
|
||||
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/nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
|
|
|
@ -9,6 +9,9 @@ function tradeMocks() {
|
|||
cy.intercept('GET', '**/api/v1/blacklist', { fixture: 'blacklist.json' }).as('Blacklist');
|
||||
cy.intercept('GET', '**/api/v1/locks', { fixture: 'locks_empty.json' }).as('Locks');
|
||||
cy.intercept('GET', '**/api/v1/performance', { fixture: 'performance.json' }).as('Performance');
|
||||
cy.intercept('POST', '**/api/v1/reload_config', { fixture: 'reload_config.json' }).as(
|
||||
'ReloadConfig',
|
||||
);
|
||||
}
|
||||
|
||||
describe('Trade', () => {
|
||||
|
@ -60,5 +63,18 @@ describe('Trade', () => {
|
|||
cy.get('.drag-header').contains('Closed Trades').scrollIntoView().should('be.visible');
|
||||
cy.get('span').contains('TRX/USDT').should('be.visible');
|
||||
cy.get('td').contains('8070.5').should('be.visible');
|
||||
// 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,
|
||||
"trade_count": null,
|
||||
"backtest_result": {
|
||||
"metadata": {
|
||||
"SampleStrategy": {
|
||||
"strategy": "SampleStrategy",
|
||||
"run_id": "0afc3cf414b6da90210f458078a6b06055b0c4e7",
|
||||
"filename": "backtest-result-2023-08-03_04-02-20"
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"SampleStrategy": {
|
||||
"trades": [
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"closed_trade_count": 149,
|
||||
"first_trade_date": "a year ago",
|
||||
"first_trade_timestamp": 1626021644985,
|
||||
"bot_start_timestamp": 1625021644985,
|
||||
"latest_trade_date": "5 days ago",
|
||||
"latest_trade_timestamp": 1661828800529,
|
||||
"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": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@vuepic/vue-datepicker": "^5.4.0",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"@vueuse/integrations": "^10.2.1",
|
||||
"axios": "^1.4.0",
|
||||
"bootstrap": "^5.3.0",
|
||||
"bootstrap-vue-next": "^0.8.13",
|
||||
"core-js": "^3.31.1",
|
||||
"@vuepic/vue-datepicker": "^6.1.0",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@vueuse/integrations": "^10.4.1",
|
||||
"axios": "^1.5.0",
|
||||
"bootstrap": "^5.3.1",
|
||||
"bootstrap-vue-next": "^0.12.3",
|
||||
"core-js": "^3.32.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"echarts": "^5.4.2",
|
||||
"echarts": "^5.4.3",
|
||||
"favico.js": "^0.3.10",
|
||||
"humanize-duration": "^3.29.0",
|
||||
"pinia": "^2.1.4",
|
||||
"pinia-plugin-persistedstate": "^3.1.0",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vue": "^3.3.2",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-demi": "^0.14.1",
|
||||
"vue-echarts": "^6.6.0",
|
||||
"vue-demi": "^0.14.6",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-select": "^4.0.0-beta.6",
|
||||
"vue3-drr-grid-layout": "^1.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/vite-dev-server": "^5.0.5",
|
||||
"@cypress/vue": "^5.0.5",
|
||||
"@iconify-json/mdi": "^1.1.53",
|
||||
"@cypress/vite-dev-server": "^5.0.6",
|
||||
"@cypress/vue": "^6.0.0",
|
||||
"@iconify-json/mdi": "^1.1.54",
|
||||
"@types/echarts": "^4.9.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"@vitejs/plugin-vue": "^4.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/runtime-dom": "^3.3.4",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"cypress": "^12.17.1",
|
||||
"eslint": "^8.44.0",
|
||||
"@vue/test-utils": "^2.4.1",
|
||||
"cypress": "^13.2.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"mutationobserver-shim": "^0.3.7",
|
||||
"portal-vue": "^3.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.63.6",
|
||||
"typescript": "~5.1.6",
|
||||
"unplugin-icons": "^0.16.3",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.3",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.4"
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.66.1",
|
||||
"typescript": "~5.2.2",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.4",
|
||||
"vue-tsc": "^1.8.11"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,18 @@
|
|||
<script setup lang="ts">
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { BotDescriptor } from '@/types';
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
bot: { type: Object as () => BotDescriptor, required: true },
|
||||
});
|
||||
const emit = defineEmits(['cancelled', 'saved']);
|
||||
const botStore = useBotStore();
|
||||
const newName = ref<string>('');
|
||||
|
||||
const newName = ref<string>(props.bot.botName);
|
||||
onMounted(() => {
|
||||
newName.value = props.bot.botName;
|
||||
});
|
||||
|
||||
const save = () => {
|
||||
botStore.updateBot(props.bot.botId, {
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
<template>
|
||||
<e-charts
|
||||
v-if="currencies"
|
||||
ref="balanceChart"
|
||||
:option="balanceChartOptions"
|
||||
:theme="settingsStore.chartTheme"
|
||||
:style="{ height: width * 0.6 + 'px' }"
|
||||
autoresize
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ECharts from 'vue-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 { LabelLayout } from 'echarts/features';
|
||||
import {
|
||||
DatasetComponent,
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
} 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 { computed } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { BalanceValues } from '@/types';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
use([
|
||||
PieChart,
|
||||
|
@ -37,6 +40,9 @@ use([
|
|||
LabelLayout,
|
||||
]);
|
||||
|
||||
const balanceChart = ref(null);
|
||||
const { width } = useElementSize(balanceChart);
|
||||
|
||||
const props = defineProps({
|
||||
currencies: { required: true, type: Array as () => BalanceValues[] },
|
||||
showTitle: { required: false, type: Boolean },
|
||||
|
@ -93,8 +99,6 @@ const balanceChartOptions = computed((): EChartsOption => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.echarts {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 240px;
|
||||
min-height: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -37,6 +37,9 @@ import {
|
|||
TooltipComponent,
|
||||
VisualMapComponent,
|
||||
VisualMapPiecewiseComponent,
|
||||
// MarkAreaComponent,
|
||||
MarkLineComponent,
|
||||
// MarkPointComponent,
|
||||
} from 'echarts/components';
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
@ -54,6 +57,9 @@ use([
|
|||
TooltipComponent,
|
||||
VisualMapComponent,
|
||||
VisualMapPiecewiseComponent,
|
||||
// MarkAreaComponent,
|
||||
MarkLineComponent,
|
||||
// MarkPointComponent,
|
||||
|
||||
CandlestickChart,
|
||||
BarChart,
|
||||
|
@ -135,7 +141,8 @@ function updateChart(initial = false) {
|
|||
if (chartOptions.value?.title) {
|
||||
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 colOpen = columns.findIndex((el) => el === 'open');
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<b-button class="ms-2" :disabled="!!!pair" size="sm" @click="refresh">
|
||||
<i-mdi-refresh />
|
||||
</b-button>
|
||||
<div class="d-flex flex-row flex-wrap">
|
||||
<small v-if="dataset" class="ms-2 text-nowrap" title="Long entry signals"
|
||||
>Long signals: {{ dataset.enter_long_signals || dataset.buy_signals }}</small
|
||||
>
|
||||
|
@ -31,9 +32,10 @@
|
|||
>Short exits: {{ dataset.exit_short_signals }}</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto d-flex align-items-center w-auto">
|
||||
<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">
|
||||
|
@ -121,7 +123,7 @@ const botStore = useBotStore();
|
|||
const plotStore = usePlotConfigStore();
|
||||
|
||||
const pair = ref('');
|
||||
const showPlotConfig = ref(props.plotConfigModal);
|
||||
const showPlotConfig = ref<boolean>();
|
||||
|
||||
const dataset = computed((): PairHistory => {
|
||||
if (props.historicView) {
|
||||
|
@ -159,14 +161,16 @@ const noDatasetText = computed((): string => {
|
|||
}
|
||||
});
|
||||
const showPlotConfigModal = ref(false);
|
||||
const showConfigurator = () => {
|
||||
|
||||
function showConfigurator() {
|
||||
if (props.plotConfigModal) {
|
||||
showPlotConfigModal.value = !showPlotConfigModal.value;
|
||||
} else {
|
||||
showPlotConfig.value = !showPlotConfig.value;
|
||||
}
|
||||
};
|
||||
const refresh = () => {
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
console.log('refresh', pair.value, props.timeframe);
|
||||
if (pair.value && props.timeframe) {
|
||||
if (props.historicView) {
|
||||
|
@ -181,11 +185,10 @@ const refresh = () => {
|
|||
botStore.activeBot.getPairCandles({
|
||||
pair: pair.value,
|
||||
timeframe: props.timeframe,
|
||||
limit: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.availablePairs,
|
||||
|
@ -206,6 +209,7 @@ watch(
|
|||
);
|
||||
|
||||
onMounted(() => {
|
||||
showPlotConfig.value = props.plotConfigModal;
|
||||
if (botStore.activeBot.selectedPair) {
|
||||
pair.value = botStore.activeBot.selectedPair;
|
||||
} else if (props.availablePairs.length > 0) {
|
||||
|
|
|
@ -56,7 +56,6 @@ const props = defineProps({
|
|||
});
|
||||
const settingsStore = useSettingsStore();
|
||||
// const botList = ref<string[]>([]);
|
||||
// const cumulativeData = ref<{ date: number; profit: any }[]>([]);
|
||||
|
||||
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];
|
||||
if (lastPoint) {
|
||||
const resultWitHOpen = (lastPoint.profit ?? 0) + openProfit.value;
|
||||
valueArray.push({ date: lastPoint.date, currentProfit: lastPoint.profit });
|
||||
lastProfit = lastPoint.profit ?? 0;
|
||||
lastDate = lastPoint.date ?? 0;
|
||||
} 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
|
||||
const tomorrow = Date.now() + 24 * 60 * 60 * 1000;
|
||||
valueArray.push({ date: tomorrow, currentProfit: resultWitHOpen });
|
||||
}
|
||||
}
|
||||
return valueArray;
|
||||
});
|
||||
|
||||
function updateChart(initial = false) {
|
||||
function generateChart(initial = false) {
|
||||
const { colorProfit, colorLoss } = settingsStore;
|
||||
const chartOptionsLoc: EChartsOption = {
|
||||
dataset: {
|
||||
|
@ -167,7 +172,6 @@ function updateChart(initial = false) {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
// TODO: maybe have profit lines per bot?
|
||||
// this.botList.forEach((botId: string) => {
|
||||
// console.log('bot', botId);
|
||||
|
@ -184,6 +188,10 @@ function updateChart(initial = false) {
|
|||
// // symbol: 'none',
|
||||
// });
|
||||
// });
|
||||
return chartOptionsLoc;
|
||||
}
|
||||
function updateChart(initial = false) {
|
||||
const chartOptionsLoc = generateChart(initial);
|
||||
chart.value?.setOption(chartOptionsLoc, {
|
||||
replaceMerge: ['series', 'dataset'],
|
||||
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 });
|
||||
updateChart(true);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
v-model="fillTo"
|
||||
:columns="columns"
|
||||
class="mt-1"
|
||||
label="Select indicator to add"
|
||||
label="Area chart - Fill to (leave empty for line chart)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import vSelect from 'vue-select';
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -27,7 +27,7 @@ const props = defineProps({
|
|||
});
|
||||
const emit = defineEmits(['update:modelValue', 'indicatorSelected']);
|
||||
|
||||
const selAvailableIndicator = ref(props.modelValue || '');
|
||||
const selAvailableIndicator = ref('');
|
||||
|
||||
function emitIndicator() {
|
||||
emit('indicatorSelected', selAvailableIndicator.value);
|
||||
|
@ -37,6 +37,11 @@ function abort() {
|
|||
selAvailableIndicator.value = '';
|
||||
emitIndicator();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selAvailableIndicator.value = props.modelValue;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<template>
|
||||
<e-charts
|
||||
v-if="dailyStats.data"
|
||||
ref="dailyChart"
|
||||
:option="dailyChartOptions"
|
||||
:theme="settingsStore.chartTheme"
|
||||
:style="{ height: width * 0.6 + 'px' }"
|
||||
autoresize
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import { computed, ComputedRef, ref } from 'vue';
|
||||
import ECharts from 'vue-echarts';
|
||||
// import { EChartsOption } from 'echarts';
|
||||
|
||||
|
@ -24,9 +26,10 @@ import {
|
|||
VisualMapComponent,
|
||||
} from 'echarts/components';
|
||||
|
||||
import { DailyReturnValue } from '@/types';
|
||||
import { TimeSummaryReturnValue } from '@/types';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
|
||||
use([
|
||||
BarChart,
|
||||
|
@ -46,7 +49,7 @@ const CHART_TRADE_COUNT = 'Trade Count';
|
|||
|
||||
const props = defineProps({
|
||||
dailyStats: {
|
||||
type: Object as () => DailyReturnValue,
|
||||
type: Object as () => TimeSummaryReturnValue,
|
||||
required: true,
|
||||
},
|
||||
showTitle: {
|
||||
|
@ -56,6 +59,10 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const dailyChart = ref(null);
|
||||
const { width } = useElementSize(dailyChart);
|
||||
|
||||
const absoluteMin = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
||||
|
@ -125,7 +132,7 @@ const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
|
|||
},
|
||||
nameRotate: 90,
|
||||
nameLocation: 'middle',
|
||||
nameGap: 40,
|
||||
nameGap: 35,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
|
@ -156,8 +163,6 @@ const dailyChartOptions: ComputedRef<EChartsOption> = computed(() => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.echarts {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 240px;
|
||||
}
|
||||
</style>
|
|
@ -12,32 +12,64 @@
|
|||
Load Historic results from disk. You can click on multiple results to load all of them into
|
||||
freqUI.
|
||||
</p>
|
||||
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ms-2">
|
||||
<b-list-group v-if="botStore.activeBot.backtestHistoryList" class="ms-2 mb-1">
|
||||
<b-list-group-item
|
||||
v-for="(res, idx) in botStore.activeBot.backtestHistoryList"
|
||||
: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
|
||||
:disabled="res.run_id in botStore.activeBot.backtestHistory"
|
||||
@click="botStore.activeBot.getBacktestHistoryResult(res)"
|
||||
>
|
||||
<strong>{{ res.strategy }}</strong>
|
||||
backtested on: {{ timestampms(res.backtest_start_time * 1000) }}
|
||||
<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>
|
||||
</div>
|
||||
<MessageBox ref="msgBox" />
|
||||
</template>
|
||||
|
||||
<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 { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { BacktestHistoryEntry } from '@/types';
|
||||
import InfoBox from '../general/InfoBox.vue';
|
||||
|
||||
const botStore = useBotStore();
|
||||
const msgBox = ref<typeof MessageBox>();
|
||||
|
||||
onMounted(() => {
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div class="container-fluid px-0 backtestresult-container">
|
||||
<div class="row d-flex justify-content-center">
|
||||
<div class="px-0 mw-100">
|
||||
<div class="d-flex justify-content-center">
|
||||
<h3>Backtest-result for {{ backtestResult.strategy_name }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="row text-start ms-0">
|
||||
<div class="row w-100">
|
||||
<div class="col-12 col-xl-6 px-0 px-xl-0 pe-xl-1">
|
||||
<div class="d-flex flex-column text-start ms-0 me-2 gap-2">
|
||||
<div class="d-flex flex-column flex-xl-row">
|
||||
<div class="px-0 px-xl-0 pe-xl-1 flex-fill">
|
||||
<b-card header="Strategy settings">
|
||||
<b-table
|
||||
small
|
||||
|
@ -17,14 +17,14 @@
|
|||
</b-table>
|
||||
</b-card>
|
||||
</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-table small borderless :items="backtestResultStats" :fields="backtestResultFields">
|
||||
</b-table>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
<b-card header="Results per Exit-reason" class="row mt-2 w-100">
|
||||
<b-card header="Results per Exit-reason">
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
|
@ -37,7 +37,7 @@
|
|||
>
|
||||
</b-table>
|
||||
</b-card>
|
||||
<b-card header="Results per pair" class="row mt-2 w-100">
|
||||
<b-card header="Results per pair">
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
|
@ -47,18 +47,13 @@
|
|||
>
|
||||
</b-table>
|
||||
</b-card>
|
||||
<b-card
|
||||
v-if="backtestResult.periodic_breakdown"
|
||||
header="Periodic breakdown"
|
||||
class="row mt-2 w-100"
|
||||
>
|
||||
<b-card v-if="backtestResult.periodic_breakdown" header="Periodic breakdown">
|
||||
<BacktestResultPeriodBreakdown :periodic-breakdown="backtestResult.periodic_breakdown">
|
||||
</BacktestResultPeriodBreakdown>
|
||||
</b-card>
|
||||
|
||||
<b-card header="Single trades" class="row mt-2 w-100">
|
||||
<b-card header="Single trades">
|
||||
<TradeList
|
||||
class="row trade-history mt-2 w-100"
|
||||
:trades="backtestResult.trades"
|
||||
:show-filter="true"
|
||||
:stake-currency="backtestResult.stake_currency"
|
||||
|
@ -79,6 +74,7 @@ import {
|
|||
formatPercent,
|
||||
formatPrice,
|
||||
humanizeDurationFromSeconds,
|
||||
isNotUndefined,
|
||||
} from '@/shared/formatters';
|
||||
import { TableField, TableItem } from 'bootstrap-vue-next';
|
||||
|
||||
|
@ -115,6 +111,10 @@ const worstPair = computed((): string => {
|
|||
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(() => {
|
||||
// Transpose Result into readable format
|
||||
const shortMetrics =
|
||||
|
@ -164,9 +164,16 @@ const backtestResultStats = computed(() => {
|
|||
value: `${props.backtestResult.calmar ? props.backtestResult.calmar.toFixed(2) : 'N/A'}`,
|
||||
},
|
||||
{
|
||||
metric: 'Expectancy',
|
||||
metric: `Expectancy ${props.backtestResult.expectancy_ratio ? '(ratio)' : ''}`,
|
||||
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',
|
||||
value: `${
|
||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1].wins
|
||||
} / ${
|
||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
||||
.draws
|
||||
} / ${
|
||||
props.backtestResult.results_per_pair[props.backtestResult.results_per_pair.length - 1]
|
||||
.losses
|
||||
value: `${pairSummary.value.wins} / ${pairSummary.value.draws} / ${
|
||||
pairSummary.value.losses
|
||||
} ${
|
||||
isNotUndefined(pairSummary.value.winrate)
|
||||
? '(WR: ' +
|
||||
formatPercent(
|
||||
props.backtestResult.results_per_pair[
|
||||
props.backtestResult.results_per_pair.length - 1
|
||||
].winrate ?? 0,
|
||||
2,
|
||||
) +
|
||||
')'
|
||||
: ''
|
||||
}`,
|
||||
},
|
||||
{
|
||||
|
@ -221,6 +233,13 @@ const backtestResultStats = computed(() => {
|
|||
metric: 'Avg. Duration Losers',
|
||||
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: 'Entry/Exit timeouts',
|
||||
|
@ -413,10 +432,4 @@ const backtestsettingFields: TableField[] = [
|
|||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.backtestresult-container {
|
||||
@media (min-width: 1200px) {
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
|
@ -15,7 +15,7 @@ const periodicBreakdownSelections = [
|
|||
{ value: 'month', text: 'Months' },
|
||||
];
|
||||
|
||||
const periodicBreakdownPeriod = ref<string>('day');
|
||||
const periodicBreakdownPeriod = ref<string>('month');
|
||||
|
||||
const periodicBreakdownFields = computed<TableField[]>(() => {
|
||||
return [
|
||||
|
|
|
@ -1,16 +1,56 @@
|
|||
<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>
|
||||
<b-list-group class="ms-2">
|
||||
<b-list-group-item
|
||||
v-for="[key, strat] in Object.entries(backtestHistory)"
|
||||
v-for="[key, result] in Object.entries(backtestHistory)"
|
||||
:key="key"
|
||||
button
|
||||
: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)"
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
|
@ -18,19 +58,37 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { StrategyBacktestResult } from '@/types';
|
||||
import { BacktestResultInMemory, BacktestResultUpdate } from '@/types';
|
||||
|
||||
defineProps({
|
||||
backtestHistory: {
|
||||
required: true,
|
||||
type: Object as () => Record<string, StrategyBacktestResult>,
|
||||
type: Object as () => Record<string, BacktestResultInMemory>,
|
||||
},
|
||||
selectedBacktestResultKey: { required: false, default: '', type: String },
|
||||
canUseModify: { required: false, default: false, type: Boolean },
|
||||
});
|
||||
const emit = defineEmits(['selectionChange']);
|
||||
const setBacktestResult = (key) => {
|
||||
const emit = defineEmits<{
|
||||
selectionChange: [value: string];
|
||||
removeResult: [value: string];
|
||||
updateResult: [value: BacktestResultUpdate];
|
||||
}>();
|
||||
|
||||
const setBacktestResult = (key: string) => {
|
||||
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>
|
||||
|
||||
<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>
|
||||
<b-table class="table-sm" :items="balanceCurrencies" :fields="tableFields">
|
||||
<template #custom-foot>
|
||||
<td><strong>Total</strong></td>
|
||||
<td>
|
||||
<td class="pt-1"><strong>Total</strong></td>
|
||||
<td class="pt-1">
|
||||
<span
|
||||
class="font-italic"
|
||||
:title="`Increase over initial capital of ${formatCurrency(
|
||||
botStore.activeBot.balance.starting_capital,
|
||||
)} ${botStore.activeBot.balance.stake}`"
|
||||
>{{ formatPercent(botStore.activeBot.balance.starting_capital_ratio) }}</span
|
||||
>
|
||||
{{ formatPercent(botStore.activeBot.balance.starting_capital_ratio) }}
|
||||
</span>
|
||||
</td>
|
||||
<!-- this is a computed prop that adds up all the expenses in the visible rows -->
|
||||
<td>
|
||||
<strong>{{
|
||||
<td class="pt-1">
|
||||
<strong>
|
||||
{{
|
||||
showBotOnly && canUseBotBalance
|
||||
? formatCurrency(botStore.activeBot.balance.total_bot)
|
||||
: formatCurrency(botStore.activeBot.balance.total)
|
||||
}}</strong>
|
||||
}}
|
||||
</strong>
|
||||
</td>
|
||||
</template>
|
||||
</b-table>
|
||||
|
|
|
@ -12,41 +12,50 @@
|
|||
<div class="d-flex flex-row">
|
||||
<b-form-checkbox
|
||||
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"
|
||||
/>
|
||||
<span>{{ row.value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(profitOpen)="row">
|
||||
<template #cell(profitOpen)="{ item }">
|
||||
<profit-pill
|
||||
v-if="row.item.profitOpen && row.item.botId != 'Summary'"
|
||||
:profit-ratio="row.item.profitOpenRatio"
|
||||
:profit-abs="row.item.profitOpen"
|
||||
:stake-currency="row.item.stakeCurrency"
|
||||
v-if="item.profitOpen && item.botId != 'Summary'"
|
||||
:profit-ratio="(item as unknown as ComparisonTableItems).profitOpenRatio"
|
||||
:profit-abs="(item as unknown as ComparisonTableItems).profitOpen"
|
||||
:stake-currency="(item as unknown as ComparisonTableItems).stakeCurrency"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(profitClosed)="row">
|
||||
<template #cell(profitClosed)="{ item }">
|
||||
<profit-pill
|
||||
v-if="row.item.profitClosed && row.item.botId != 'Summary'"
|
||||
:profit-ratio="row.item.profitClosedRatio"
|
||||
:profit-abs="row.item.profitClosed"
|
||||
:stake-currency="row.item.stakeCurrency"
|
||||
v-if="item.profitClosed && item.botId != 'Summary'"
|
||||
:profit-ratio="(item as unknown as ComparisonTableItems).profitClosedRatio"
|
||||
:profit-abs="(item as unknown as ComparisonTableItems).profitClosed"
|
||||
:stake-currency="(item as unknown as ComparisonTableItems).stakeCurrency"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(balance)="row">
|
||||
<div v-if="row.item.balance">
|
||||
<span :title="row.item.stakeCurrency"
|
||||
>{{ formatPrice(row.item.balance, row.item.stakeCurrencyDecimals) }}
|
||||
<template #cell(balance)="{ item }">
|
||||
<div v-if="item.balance">
|
||||
<span :title="(item as unknown as ComparisonTableItems).stakeCurrency"
|
||||
>{{
|
||||
formatPrice(
|
||||
(item as unknown as ComparisonTableItems).balance ?? 0,
|
||||
(item as unknown as ComparisonTableItems).stakeCurrencyDecimals,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="text-small">{{ row.item.stakeCurrency }}</span>
|
||||
<span class="text-small">{{
|
||||
` ${item.stakeCurrency}${item.isDryRun ? ' (dry)' : ''}`
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(winVsLoss)="row">
|
||||
<div v-if="row.item.losses !== undefined">
|
||||
<span class="text-profit">{{ row.item.wins }}</span> /
|
||||
<span class="text-loss">{{ row.item.losses }}</span>
|
||||
<template #cell(winVsLoss)="{ item }">
|
||||
<div v-if="item.losses !== undefined">
|
||||
<span class="text-profit">{{ item.wins }}</span> /
|
||||
<span class="text-loss">{{ item.losses }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
|
@ -113,13 +122,20 @@ const tableItems = computed<TableItem[]>(() => {
|
|||
losses: v.losing_trades,
|
||||
balance: botStore.allBalance[k]?.total_bot ?? botStore.allBalance[k]?.total,
|
||||
stakeCurrencyDecimals: botStore.allBotState[k]?.stake_currency_decimals || 3,
|
||||
isDryRun: botStore.allBotState[k]?.dry_run,
|
||||
});
|
||||
if (v.profit_closed_coin !== undefined) {
|
||||
if (botStore.botStores[k].isSelected) {
|
||||
// Summary should only include selected bots
|
||||
summary.profitClosed += v.profit_closed_coin;
|
||||
summary.profitOpen += profitOpen;
|
||||
summary.wins += v.winning_trades;
|
||||
summary.losses += v.losing_trades;
|
||||
// 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);
|
||||
|
|
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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
||||
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';
|
||||
|
||||
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>
|
||||
<!-- TODO We could move the list into a component since we are reusing the same code for both lists. -->
|
||||
<div>
|
||||
<div>
|
||||
<h3>Whitelist Methods</h3>
|
||||
|
||||
<div v-if="botStore.activeBot.pairlistMethods.length" class="list">
|
||||
<b-list-group v-for="(method, key) in botStore.activeBot.pairlistMethods" :key="key">
|
||||
<b-list-group-item href="#" class="pair white">{{ method }}</b-list-group-item>
|
||||
</b-list-group>
|
||||
<div v-if="botStore.activeBot.pairlistMethods.length" class="list wide">
|
||||
<div
|
||||
v-for="(method, key) in botStore.activeBot.pairlistMethods"
|
||||
:key="key"
|
||||
class="pair white align-middle border border-secondary"
|
||||
>
|
||||
{{ method }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Show Whitelist -->
|
||||
<h3 :title="`${botStore.activeBot.whitelist.length} pairs`">Whitelist</h3>
|
||||
<div v-if="botStore.activeBot.whitelist.length" class="list">
|
||||
<b-list-group v-for="(pair, key) in botStore.activeBot.whitelist" :key="key">
|
||||
<b-list-group-item class="pair white">{{ pair }}</b-list-group-item>
|
||||
</b-list-group>
|
||||
<div
|
||||
v-for="(pair, key) in botStore.activeBot.whitelist"
|
||||
:key="key"
|
||||
class="pair white align-middle border border-secondary text-small"
|
||||
>
|
||||
{{ pair }}
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>List Unavailable. Please Login and make sure server is running.</p>
|
||||
<hr />
|
||||
|
@ -76,14 +83,15 @@
|
|||
</b-popover>
|
||||
</div>
|
||||
<div v-if="botStore.activeBot.blacklist.length" class="list">
|
||||
<b-list-group v-for="(pair, key) in botStore.activeBot.blacklist" :key="key">
|
||||
<b-list-group-item
|
||||
class="pair black"
|
||||
:active="blacklistSelect.indexOf(key) > -1"
|
||||
<div
|
||||
v-for="(pair, key) in botStore.activeBot.blacklist"
|
||||
:key="key"
|
||||
class="pair black border border-secondary"
|
||||
:class="blacklistSelect.indexOf(key) > -1 ? 'active' : ''"
|
||||
@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>
|
||||
<p v-else>BlackList Unavailable. Please Login and make sure server is running.</p>
|
||||
<!-- Pagination -->
|
||||
|
@ -119,7 +127,6 @@ const addBlacklistPair = () => {
|
|||
};
|
||||
|
||||
const blacklistSelectClick = (key) => {
|
||||
console.log(key);
|
||||
const index = blacklistSelect.value.indexOf(key);
|
||||
if (index > -1) {
|
||||
blacklistSelect.value.splice(index, 1);
|
||||
|
@ -162,7 +169,7 @@ onMounted(() => {
|
|||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.list-group-item.active .check {
|
||||
.pair.active .check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -172,14 +179,14 @@ onMounted(() => {
|
|||
grid-gap: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.wide {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.pair {
|
||||
border: 1px solid #ccc;
|
||||
background: #41b883;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
class="btn-xs ms-1"
|
||||
size="sm"
|
||||
title="Delete trade"
|
||||
@click="removePairLock(row.item)"
|
||||
@click="removePairLock(row.item as unknown as Lock)"
|
||||
>
|
||||
<i-mdi-delete />
|
||||
</b-button>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
v-model="pairlistStore.configName"
|
||||
size="sm"
|
||||
:options="pairlistStore.savedConfigs.map((c) => c.name)"
|
||||
@change="(config) => pairlistStore.selectOrCreateConfig(config)"
|
||||
@change="(config) => pairlistStore.selectOrCreateConfig(config as string)"
|
||||
/>
|
||||
</edit-value>
|
||||
<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 '';
|
||||
});
|
||||
|
||||
const updateInput = () => {
|
||||
function updateInput() {
|
||||
const tr = props.modelValue.split('-');
|
||||
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 {
|
||||
dateFrom.value = '';
|
||||
}
|
||||
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 {
|
||||
dateTo.value = '';
|
||||
}
|
||||
emit('update:modelValue', timeRange.value);
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => timeRange.value,
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<i-mdi-close-box-multiple class="me-1" />Forceexit partial
|
||||
</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"
|
||||
size="sm"
|
||||
title="Cancel open orders"
|
||||
|
|
|
@ -26,12 +26,12 @@
|
|||
@row-clicked="onRowClicked"
|
||||
@row-selected="onRowSelected"
|
||||
>
|
||||
<template #cell(actions)="row">
|
||||
<template #cell(actions)="{ index, item }">
|
||||
<TradeActionsPopover
|
||||
:id="row.index"
|
||||
:trade="row.item"
|
||||
:id="index"
|
||||
:trade="item as unknown as Trade"
|
||||
:bot-api-version="botStore.activeBot.botApiVersion"
|
||||
@delete-trade="removeTradeHandler(row.item)"
|
||||
@delete-trade="removeTradeHandler(item as unknown as Trade)"
|
||||
@force-exit="forceExitHandler"
|
||||
@force-exit-partial="forceExitPartialHandler"
|
||||
@cancel-open-order="cancelOpenOrderHandler"
|
||||
|
@ -40,11 +40,7 @@
|
|||
</template>
|
||||
<template #cell(pair)="row">
|
||||
<span>
|
||||
{{
|
||||
`${row.item.pair}${
|
||||
row.item.open_order_id === undefined || row.item.open_order_id === null ? '' : '*'
|
||||
}`
|
||||
}}
|
||||
{{ `${row.item.pair}${row.item.open_order_id || row.item.has_open_orders ? '*' : ''}` }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell(trade_id)="row">
|
||||
|
@ -60,13 +56,13 @@
|
|||
{{ row.item.trading_mode !== 'spot' ? `(${row.item.leverage}x)` : '' }}
|
||||
</template>
|
||||
<template #cell(profit)="row">
|
||||
<trade-profit :trade="row.item" />
|
||||
<trade-profit :trade="row.item as unknown as Trade" />
|
||||
</template>
|
||||
<template #cell(open_timestamp)="row">
|
||||
<DateTimeTZ :date="row.item.open_timestamp" />
|
||||
<DateTimeTZ :date="(row.item as unknown as Trade).open_timestamp" />
|
||||
</template>
|
||||
<template #cell(close_timestamp)="row">
|
||||
<DateTimeTZ :date="row.item.close_timestamp" />
|
||||
<DateTimeTZ :date="(row.item as unknown as Trade).close_timestamp ?? 0" />
|
||||
</template>
|
||||
</b-table>
|
||||
<div class="w-100 d-flex justify-content-between">
|
||||
|
@ -97,7 +93,7 @@ import TradeProfit from './TradeProfit.vue';
|
|||
import TradeActionsPopover from './TradeActionsPopover.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 { useRouter } from 'vue-router';
|
||||
import { TableField, TableItem } from 'bootstrap-vue-next';
|
||||
|
@ -143,7 +139,12 @@ const rows = computed(() => {
|
|||
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: 'pair', label: 'Pair' },
|
||||
{ key: 'amount', label: 'Amount' },
|
||||
|
@ -164,7 +165,6 @@ const tableFields: TableField[] = [
|
|||
{
|
||||
key: 'profit',
|
||||
label: props.activeTrades ? 'Current profit %' : 'Profit %',
|
||||
|
||||
formatter: (value: unknown, key?: string, item?: unknown) => {
|
||||
if (!item) {
|
||||
return '';
|
||||
|
@ -178,8 +178,9 @@ const tableFields: TableField[] = [
|
|||
...(props.activeTrades ? openFields : closedFields),
|
||||
];
|
||||
if (props.multiBotView) {
|
||||
tableFields.unshift({ key: 'botName', label: 'Bot' });
|
||||
tableFields.value.unshift({ key: 'botName', label: 'Bot' });
|
||||
}
|
||||
});
|
||||
|
||||
const feOrderType = ref<string | undefined>(undefined);
|
||||
const forceExitHandler = (item: Trade, ordertype: string | undefined = undefined) => {
|
||||
|
|
|
@ -9,26 +9,48 @@
|
|||
>Trade Navigation {{ sortNewestFirst ? '↓' : '↑' }}
|
||||
</b-list-group-item>
|
||||
<b-list-group-item
|
||||
v-for="trade in sortedTrades"
|
||||
v-for="(trade, i) in sortedTrades"
|
||||
:key="trade.open_timestamp"
|
||||
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}`"
|
||||
:active="trade.open_timestamp === selectedTrade.open_timestamp"
|
||||
@click="onTradeSelect(trade)"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<div>
|
||||
<span v-if="botStore.activeBot.botState.trading_mode !== 'spot'">{{
|
||||
trade.is_short ? 'S-' : 'L-'
|
||||
}}</span>
|
||||
<DateTimeTZ :date="trade.open_timestamp" />
|
||||
</div>
|
||||
<TradeProfit :trade="trade" />
|
||||
<TradeProfit :trade="trade" class="my-1" />
|
||||
<ProfitPill
|
||||
v-if="backtestMode"
|
||||
:profit-ratio="trade.profit_ratio"
|
||||
: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 v-if="trades.length === 0">No trades to show...</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
@ -39,7 +61,7 @@
|
|||
import { Trade } from '@/types';
|
||||
import TradeProfit from '@/components/ftbot/TradeProfit.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 DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
|
||||
|
@ -67,6 +89,15 @@ const sortedTrades = computed(() => {
|
|||
: a.open_timestamp - b.open_timestamp,
|
||||
);
|
||||
});
|
||||
|
||||
const ordersVisible = ref(sortedTrades.value.map(() => false));
|
||||
|
||||
watch(
|
||||
() => botStore.activeBot.selectedPair,
|
||||
() => {
|
||||
ordersVisible.value = sortedTrades.value.map(() => false);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
|
||||
import {
|
||||
timestampmsOrNa,
|
||||
timestampmsWithTimezone,
|
||||
timestampToDateString,
|
||||
} from '@/shared/formatters';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
|
@ -19,7 +23,7 @@ const formattedDate = computed((): string => {
|
|||
if (props.showTimezone) {
|
||||
return timestampmsWithTimezone(props.date);
|
||||
}
|
||||
return timestampms(props.date);
|
||||
return timestampmsOrNa(props.date);
|
||||
});
|
||||
|
||||
const timezoneTooltip = computed((): string => {
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -104,8 +104,11 @@ enum EditState {
|
|||
Duplicating,
|
||||
}
|
||||
|
||||
const localName = ref<string>(props.modelValue);
|
||||
const localName = ref<string>('');
|
||||
const mode = ref<EditState>(EditState.None);
|
||||
onMounted(() => {
|
||||
localName.value = props.modelValue;
|
||||
});
|
||||
|
||||
function abort() {
|
||||
mode.value = EditState.None;
|
||||
|
|
|
@ -42,17 +42,17 @@ const routes: Array<RouteRecordRaw> = [
|
|||
},
|
||||
{
|
||||
path: '/open_trades',
|
||||
component: () => import('@/views/TradesList.vue'),
|
||||
component: () => import('@/views/TradesListView.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
path: '/trade_history',
|
||||
component: () => import('@/views/TradesList.vue'),
|
||||
component: () => import('@/views/TradesListView.vue'),
|
||||
props: { history: true },
|
||||
},
|
||||
{
|
||||
path: '/pairlist',
|
||||
component: () => import('@/components/ftbot/FTBotAPIPairList.vue'),
|
||||
component: () => import('@/components/ftbot/PairListLive.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
|
|
|
@ -12,11 +12,14 @@ export function calculateDiff(
|
|||
): number[][] {
|
||||
const fromIdx = columns.indexOf(colFrom);
|
||||
const toIdx = columns.indexOf(colTo);
|
||||
const hasBothColumns = fromIdx > 0 && toIdx > 0;
|
||||
if (hasBothColumns) {
|
||||
columns.push(`${colFrom}-${colTo}`);
|
||||
|
||||
}
|
||||
return data.map((original) => {
|
||||
// Prevent mutation of original data
|
||||
const candle = original.slice();
|
||||
if (hasBothColumns) {
|
||||
const diff =
|
||||
candle === null || candle[toIdx] === null || candle[fromIdx] === null
|
||||
? null
|
||||
|
@ -24,6 +27,7 @@ export function calculateDiff(
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
candle.push(diff);
|
||||
}
|
||||
return candle;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,11 +47,12 @@ export function getTradeEntries(dataset: PairHistory, trades: Trade[]) {
|
|||
// 4: color
|
||||
// 5: label
|
||||
// 6: tooltip
|
||||
const stop_ts_adjusted = dataset.data_stop_ts + dataset.timeframe_ms;
|
||||
for (let i = 0, len = trades.length; i < len; i += 1) {
|
||||
const trade: Trade = trades[i];
|
||||
if (
|
||||
// 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 >= dataset.data_start_ts)
|
||||
) {
|
||||
|
@ -61,7 +62,7 @@ export function getTradeEntries(dataset: PairHistory, trades: Trade[]) {
|
|||
if (
|
||||
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
|
||||
) {
|
||||
// Trade entry
|
||||
|
@ -79,7 +80,7 @@ export function getTradeEntries(dataset: PairHistory, trades: Trade[]) {
|
|||
} else if (i === trade.orders.length - 1 && trade.close_timestamp) {
|
||||
if (
|
||||
roundTimeframe(dataset.timeframe_ms ?? 0, trade.close_timestamp) <=
|
||||
dataset.data_stop_ts &&
|
||||
stop_ts_adjusted &&
|
||||
trade.close_timestamp > dataset.data_start_ts &&
|
||||
trade.is_open === false
|
||||
) {
|
||||
|
@ -127,6 +128,8 @@ export function generateTradeSeries(
|
|||
): ScatterSeriesOption {
|
||||
const { tradeData } = getTradeEntries(dataset, trades);
|
||||
|
||||
const openTrades = trades.filter((t) => t.is_open);
|
||||
|
||||
const tradesSeries: ScatterSeriesOption = {
|
||||
name: nameTrades,
|
||||
type: 'scatter',
|
||||
|
@ -158,6 +161,41 @@ export function generateTradeSeries(
|
|||
symbolSize: 13,
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export function formatPrice(value: number | null, decimals = 15): string {
|
|||
* @returns
|
||||
*/
|
||||
export function formatPriceCurrency(price: number | null, currency: string, decimals = 3) {
|
||||
return `${formatPrice(price, decimals)} ${currency}`;
|
||||
return `${formatPrice(price, decimals)} ${currency ?? ''}`;
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -38,13 +38,25 @@ export function timestampms(ts: number | Date): string {
|
|||
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
|
||||
* @param ts Timestamp as number or date (in utc!!)
|
||||
* @param timezone timezone to use
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
|
|
@ -232,7 +232,6 @@ export class UserService {
|
|||
* Call on startup to migrate old login info to new login
|
||||
*/
|
||||
public static migrateLogin() {
|
||||
// TODO: this is actually never called!
|
||||
const AUTH_REFRESH_TOKEN = 'auth_ref_token'; // Legacy key - do not use
|
||||
const AUTH_ACCESS_TOKEN = 'auth_access_token';
|
||||
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,
|
||||
StrategyResult,
|
||||
BalanceInterface,
|
||||
DailyReturnValue,
|
||||
TimeSummaryReturnValue,
|
||||
LockResponse,
|
||||
ProfitInterface,
|
||||
BacktestResult,
|
||||
StrategyBacktestResult,
|
||||
BacktestSteps,
|
||||
LogLine,
|
||||
SysInfoResponse,
|
||||
LoadingStatus,
|
||||
BacktestHistoryEntry,
|
||||
RunModes,
|
||||
DailyPayload,
|
||||
TimeSummaryPayload,
|
||||
BlacklistResponse,
|
||||
WhitelistResponse,
|
||||
StrategyListResult,
|
||||
|
@ -43,6 +42,11 @@ import {
|
|||
PairlistEvalResponse,
|
||||
PairlistsPayload,
|
||||
PairlistsResponse,
|
||||
BacktestResultInMemory,
|
||||
BacktestMetadataWithStrategyName,
|
||||
BacktestMetadataPatch,
|
||||
BacktestResultUpdate,
|
||||
TimeSummaryOptions,
|
||||
} from '@/types';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { defineStore } from 'pinia';
|
||||
|
@ -78,7 +82,9 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
profit: {} as ProfitInterface,
|
||||
botState: {} as BotState,
|
||||
balance: {} as BalanceInterface,
|
||||
dailyStats: {} as DailyReturnValue,
|
||||
dailyStats: {} as TimeSummaryReturnValue,
|
||||
weeklyStats: {} as TimeSummaryReturnValue,
|
||||
monthlyStats: {} as TimeSummaryReturnValue,
|
||||
pairlistMethods: [] as string[],
|
||||
detailTradeId: null as number | null,
|
||||
selectedPair: '',
|
||||
|
@ -102,7 +108,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
backtestTradeCount: 0,
|
||||
backtestResult: undefined as BacktestResult | undefined,
|
||||
selectedBacktestResultKey: '',
|
||||
backtestHistory: {} as Record<string, StrategyBacktestResult>,
|
||||
backtestHistory: {} as Record<string, BacktestResultInMemory>,
|
||||
backtestHistoryList: [] as BacktestHistoryEntry[],
|
||||
sysInfo: {} as SysInfoResponse,
|
||||
};
|
||||
|
@ -114,7 +120,8 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
stakeCurrencyDecimals: (state) => state.botState?.stake_currency_decimals || 3,
|
||||
canRunBacktest: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
||||
isWebserverMode: (state) => state.botState?.runmode === RunModes.WEBSERVER,
|
||||
selectedBacktestResult: (state) => state.backtestHistory[state.selectedBacktestResultKey],
|
||||
selectedBacktestResult: (state) =>
|
||||
state.backtestHistory[state.selectedBacktestResultKey]?.strategy || {},
|
||||
shortAllowed: (state) => state.botState?.short_allowed || false,
|
||||
openTradeCount: (state) => state.openTrades.length,
|
||||
isTrading: (state) =>
|
||||
|
@ -151,14 +158,6 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
botName: (state) => state.botState?.bot_name || 'freqtrade',
|
||||
allTrades: (state) => [...state.openTrades, ...state.trades] as Trade[],
|
||||
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: {
|
||||
botAdded() {
|
||||
|
@ -168,7 +167,6 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
try {
|
||||
const result = await api.get('/ping');
|
||||
const now = Date.now();
|
||||
// TODO: Name collision!
|
||||
this.ping = `${result.data.status} ${now.toString()}`;
|
||||
this.setIsBotOnline(true);
|
||||
return Promise.resolve();
|
||||
|
@ -363,30 +361,37 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
reject(error);
|
||||
});
|
||||
},
|
||||
getPairHistory(payload: PairHistoryPayload) {
|
||||
async getPairHistory(payload: PairHistoryPayload) {
|
||||
if (payload.pair && payload.timeframe) {
|
||||
this.historyStatus = LoadingStatus.loading;
|
||||
return api
|
||||
.get('/pair_history', {
|
||||
try {
|
||||
const { data } = await api.get('/pair_history', {
|
||||
params: { ...payload },
|
||||
timeout: 50000,
|
||||
})
|
||||
.then((result) => {
|
||||
});
|
||||
this.history = {
|
||||
[`${payload.pair}__${payload.timeframe}`]: {
|
||||
pair: payload.pair,
|
||||
timeframe: payload.timeframe,
|
||||
timerange: payload.timerange,
|
||||
data: result.data,
|
||||
data: data,
|
||||
},
|
||||
};
|
||||
this.historyStatus = LoadingStatus.success;
|
||||
})
|
||||
.catch((err) => {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
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
|
||||
const error = 'pair or timeframe or timerange not specified';
|
||||
console.error(error);
|
||||
|
@ -436,6 +441,11 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
return Promise.resolve(data);
|
||||
} catch (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);
|
||||
}
|
||||
},
|
||||
|
@ -520,11 +530,20 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async getDaily(payload: DailyPayload = {}) {
|
||||
async getTimeSummary(aggregation: TimeSummaryOptions, payload: TimeSummaryPayload = {}) {
|
||||
const { timescale = 20 } = payload;
|
||||
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;
|
||||
} else if (aggregation === TimeSummaryOptions.weekly) {
|
||||
this.weeklyStats = data;
|
||||
} else if (aggregation === TimeSummaryOptions.monthly) {
|
||||
this.monthlyStats = data;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -880,18 +899,30 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
}
|
||||
},
|
||||
async getBacktestHistory() {
|
||||
const result = await api.get<BacktestHistoryEntry[]>('/backtest/history');
|
||||
this.backtestHistoryList = result.data;
|
||||
const { data } = await api.get<BacktestHistoryEntry[]>('/backtest/history');
|
||||
this.backtestHistoryList = data;
|
||||
},
|
||||
updateBacktestResult(backtestResult: BacktestResult) {
|
||||
this.backtestResult = backtestResult;
|
||||
// TODO: Properly identify duplicates to avoid pushing the same multiple times
|
||||
Object.entries(backtestResult.strategy).forEach(([key, strat]) => {
|
||||
console.log(key, strat);
|
||||
const 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 = { ...this.backtestHistory, ...{ [stratKey]: strat } };
|
||||
this.backtestHistory = { ...this.backtestHistory, ...{ [stratKey]: btResult } };
|
||||
this.selectedBacktestResultKey = stratKey;
|
||||
});
|
||||
},
|
||||
|
@ -903,9 +934,50 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
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) {
|
||||
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() {
|
||||
try {
|
||||
const { data } = await api.get<SysInfoResponse>('/sysinfo');
|
||||
|
@ -936,7 +1008,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
// TODO: check for active bot ...
|
||||
if (pair === this.selectedPair) {
|
||||
// Reload pair candles
|
||||
this.getPairCandles({ pair, timeframe, limit: 500 });
|
||||
this.getPairCandles({ pair, timeframe });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -5,15 +5,16 @@ import {
|
|||
BotDescriptors,
|
||||
BotState,
|
||||
ClosedTrade,
|
||||
DailyPayload,
|
||||
DailyRecord,
|
||||
DailyReturnValue,
|
||||
TimeSummaryPayload,
|
||||
TimeSummaryRecord,
|
||||
TimeSummaryReturnValue,
|
||||
MultiCancelOpenOrderPayload,
|
||||
MultiDeletePayload,
|
||||
MultiForcesellPayload,
|
||||
MultiReloadTradePayload,
|
||||
ProfitInterface,
|
||||
Trade,
|
||||
TimeSummaryOptions,
|
||||
} from '@/types';
|
||||
import { defineStore } from 'pinia';
|
||||
import { createBotSubStore } from './ftbot';
|
||||
|
@ -121,9 +122,9 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
});
|
||||
return result;
|
||||
},
|
||||
allDailyStatsSelectedBots: (state): DailyReturnValue => {
|
||||
allDailyStatsSelectedBots: (state): TimeSummaryReturnValue => {
|
||||
// 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]) => {
|
||||
if (botStore.isSelected) {
|
||||
botStore.dailyStats?.data?.forEach((d) => {
|
||||
|
@ -138,7 +139,7 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
}
|
||||
});
|
||||
|
||||
const dailyReturn: DailyReturnValue = {
|
||||
const dailyReturn: TimeSummaryReturnValue = {
|
||||
stake_currency: 'USDT',
|
||||
fiat_display_currency: 'USD',
|
||||
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>[] = [];
|
||||
updates.push(this.allRefreshFrequent(false));
|
||||
updates.push(this.allRefreshSlow(true));
|
||||
// updates.push(this.getDaily());
|
||||
// updates.push(this.getTimeSummary());
|
||||
// updates.push(this.getBalance());
|
||||
await Promise.all(updates);
|
||||
console.log('refreshing_end');
|
||||
|
@ -303,12 +304,12 @@ export const useBotStore = defineStore('ftbot-wrapper', {
|
|||
}
|
||||
});
|
||||
},
|
||||
async allGetDaily(payload: DailyPayload) {
|
||||
const updates: Promise<DailyReturnValue>[] = [];
|
||||
async allGetDaily(payload: TimeSummaryPayload) {
|
||||
const updates: Promise<TimeSummaryReturnValue>[] = [];
|
||||
|
||||
this.allBotStores.forEach((bot) => {
|
||||
if (bot.isBotOnline) {
|
||||
updates.push(bot.getDaily(payload));
|
||||
updates.push(bot.getTimeSummary(TimeSummaryOptions.daily, payload));
|
||||
}
|
||||
});
|
||||
await Promise.all(updates);
|
||||
|
|
|
@ -80,6 +80,12 @@
|
|||
background: $bg-dark;
|
||||
}
|
||||
|
||||
.list-group-item.disabled,
|
||||
.list-group-item:disabled {
|
||||
color: darken($fg-color, 40%);
|
||||
background: $bg-dark;
|
||||
}
|
||||
|
||||
// .custom-select {
|
||||
// color: $fg-color;
|
||||
// // background: $bg-dark;
|
||||
|
@ -214,13 +220,14 @@
|
|||
}
|
||||
|
||||
.b-toast .toast {
|
||||
background: $bg-dark;
|
||||
color: black;
|
||||
// background: $bg-dark;
|
||||
border-color: lighten($bg-dark, 20%);
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
color: $fg-color;
|
||||
background: darken($bg-dark, 10%);
|
||||
// background: darken($bg-dark, 10%);
|
||||
border-color: lighten($bg-dark, 20%);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export interface PairResult {
|
|||
profit_total: number;
|
||||
trades: number;
|
||||
wins: number;
|
||||
winrate?: number;
|
||||
}
|
||||
|
||||
export interface ExitReasonResults {
|
||||
|
@ -48,6 +49,7 @@ export interface ExitReasonResults {
|
|||
profit_total_pct: number;
|
||||
trades: number;
|
||||
wins: number;
|
||||
winrate?: number;
|
||||
}
|
||||
|
||||
// Generated by https://quicktype.io
|
||||
|
@ -59,6 +61,7 @@ export interface PeriodicStat {
|
|||
wins: number;
|
||||
draws: number;
|
||||
loses: number;
|
||||
winrate?: number;
|
||||
}
|
||||
|
||||
export interface PeriodicBreakdown {
|
||||
|
@ -137,7 +140,6 @@ export interface StrategyBacktestResult {
|
|||
canceled_entry_orders?: number;
|
||||
replaced_entry_orders?: number;
|
||||
|
||||
// Daily stats ...
|
||||
draw_days: number;
|
||||
drawdown_end: string;
|
||||
drawdown_end_ts: number;
|
||||
|
@ -145,6 +147,8 @@ export interface StrategyBacktestResult {
|
|||
drawdown_start_ts: number;
|
||||
loser_holding_avg: string;
|
||||
loser_holding_avg_s: number;
|
||||
max_consecutive_wins?: number;
|
||||
max_consecutive_losses?: number;
|
||||
losing_days: number;
|
||||
max_drawdown: number;
|
||||
max_drawdown_account: number;
|
||||
|
@ -159,6 +163,7 @@ export interface StrategyBacktestResult {
|
|||
sharpe?: number;
|
||||
calmar?: number;
|
||||
expectancy?: number;
|
||||
expectancy_ratio?: number;
|
||||
|
||||
winner_holding_avg: string;
|
||||
winner_holding_avg_s: number;
|
||||
|
@ -177,9 +182,42 @@ export interface StrategyBacktestResult {
|
|||
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 {
|
||||
strategy: Record<string, StrategyBacktestResult>;
|
||||
strategy_comparison: Array<Record<string, string | number>>;
|
||||
metadata: Record<string, BacktestMetadata>;
|
||||
}
|
||||
|
||||
export enum BacktestSteps {
|
||||
|
@ -206,4 +244,5 @@ export interface BacktestHistoryEntry {
|
|||
strategy: string;
|
||||
run_id: string;
|
||||
backtest_start_time: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
|
|
@ -11,4 +11,5 @@ export interface ComparisonTableItems {
|
|||
losses: number;
|
||||
balance?: 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;
|
||||
}
|
||||
|
||||
export interface DailyRecord {
|
||||
export interface TimeSummaryRecord {
|
||||
/** Date in the format yyyy-mm-dd */
|
||||
[key: string]: string | number;
|
||||
date: string;
|
||||
|
@ -14,8 +19,8 @@ export interface DailyRecord {
|
|||
trade_count: number;
|
||||
}
|
||||
|
||||
export interface DailyReturnValue {
|
||||
data: DailyRecord[];
|
||||
export interface TimeSummaryReturnValue {
|
||||
data: TimeSummaryRecord[];
|
||||
fiat_display_currency: string;
|
||||
stake_currency: string;
|
||||
}
|
||||
|
|
|
@ -41,8 +41,15 @@ export interface ProfitInterface {
|
|||
profit_factor?: number;
|
||||
max_drawdown?: 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;
|
||||
/** Initial bot start date*/
|
||||
bot_start_timestamp?: number;
|
||||
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_pct?: number;
|
||||
|
||||
/** deprecated, to be replaced with "has_open_orders" */
|
||||
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 */
|
||||
is_short?: boolean;
|
||||
leverage?: number;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div class="container-fluid" style="max-height: calc(100vh - 60px)">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2"></div>
|
||||
<div class="d-flex flex-column pt-1 me-1" style="height: calc(100vh - 60px)">
|
||||
<div>
|
||||
<div class="d-flex flex-row">
|
||||
<h2 class="ms-5">Backtesting</h2>
|
||||
<p v-if="!botStore.activeBot.canRunBacktest">
|
||||
Bot must be in webserver mode to enable Backtesting.
|
||||
</p>
|
||||
<div class="row w-100">
|
||||
<h2 class="col-4 col-lg-3">Backtesting</h2>
|
||||
<div class="w-100">
|
||||
<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
|
||||
v-if="botStore.activeBot.botApiVersion >= 2.15"
|
||||
|
@ -58,22 +58,22 @@
|
|||
>Visualize result</b-form-radio
|
||||
>
|
||||
</div>
|
||||
<small
|
||||
v-show="botStore.activeBot.backtestRunning"
|
||||
class="text-end bt-running-label col-8 col-lg-3"
|
||||
<small v-show="botStore.activeBot.backtestRunning" class="text-end bt-running-label"
|
||||
>Backtest running: {{ botStore.activeBot.backtestStep }}
|
||||
{{ formatPercent(botStore.activeBot.backtestProgress, 2) }}</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-md-flex">
|
||||
</div>
|
||||
<div class="d-flex flex-md-row">
|
||||
<!-- Left bar -->
|
||||
<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
|
||||
v-if="btFormMode !== 'visualize'"
|
||||
class="align-self-start"
|
||||
aria-label="Close"
|
||||
size="sm"
|
||||
|
@ -85,224 +85,35 @@
|
|||
v-if="btFormMode !== 'visualize' && showLeftBar"
|
||||
:backtest-history="botStore.activeBot.backtestHistory"
|
||||
:selected-backtest-result-key="botStore.activeBot.selectedBacktestResultKey"
|
||||
:can-use-modify="botStore.activeBot.botApiVersion >= 2.32"
|
||||
@selection-change="botStore.activeBot.setBacktestResultKey"
|
||||
@remove-result="botStore.activeBot.removeBacktestResultFromMemory"
|
||||
@update-result="botStore.activeBot.saveBacktestResultMetadata"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
<!-- End Left bar -->
|
||||
<div class="d-flex flex-column flex-fill mw-100">
|
||||
<div class="d-md-flex">
|
||||
<div
|
||||
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 v-if="btFormMode == 'run'" class="flex-fill row d-flex flex-column bt-config">
|
||||
<div class="mb-2">
|
||||
<span>Strategy</span>
|
||||
<StrategySelect v-model="strategy"></StrategySelect>
|
||||
<div v-if="btFormMode == 'run'" class="flex-fill d-flex flex-column bt-config">
|
||||
<BacktestRun />
|
||||
</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="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
|
||||
<BacktestResultAnalysis
|
||||
v-if="hasBacktestResult && btFormMode == 'results'"
|
||||
:backtest-result="botStore.activeBot.selectedBacktestResult"
|
||||
class="flex-fill"
|
||||
/>
|
||||
|
||||
<BacktestGraphsView
|
||||
<BacktestGraphs
|
||||
v-if="hasBacktestResult && btFormMode == 'visualize-summary'"
|
||||
:trades="botStore.activeBot.selectedBacktestResult.trades"
|
||||
class="flex-fill"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -312,35 +123,33 @@
|
|||
>
|
||||
<BacktestResultChart
|
||||
:timeframe="timeframe"
|
||||
:strategy="strategy"
|
||||
:timerange="timerange"
|
||||
:strategy="btStore.strategy"
|
||||
:timerange="btStore.timerange"
|
||||
:pairlist="botStore.activeBot.selectedBacktestResult.pairlist"
|
||||
: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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TimeRangeSelect from '@/components/ftbot/TimeRangeSelect.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 BacktestGraphs from '@/components/ftbot/BacktestGraphs.vue';
|
||||
import BacktestHistoryLoad from '@/components/ftbot/BacktestHistoryLoad.vue';
|
||||
import BacktestGraphsView from '@/components/ftbot/BacktestGraphsView.vue';
|
||||
import BacktestResultChart from '@/components/ftbot/BacktestResultChart.vue';
|
||||
import InfoBox from '@/components/general/InfoBox.vue';
|
||||
|
||||
import { BacktestPayload } from '@/types';
|
||||
import BacktestResultSelect from '@/components/ftbot/BacktestResultSelect.vue';
|
||||
import BacktestResultAnalysis from '@/components/ftbot/BacktestResultAnalysis.vue';
|
||||
import BacktestRun from '@/components/ftbot/BacktestRun.vue';
|
||||
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { useBtStore } from '@/stores/btStore';
|
||||
import { useBotStore } from '@/stores/ftbotwrapper';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const botStore = useBotStore();
|
||||
const btStore = useBtStore();
|
||||
|
||||
const hasBacktestResult = computed(() =>
|
||||
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 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 pollInterval = ref<number | null>(null);
|
||||
|
||||
const selectBacktestResult = () => {
|
||||
// Set parameters for this result
|
||||
strategy.value = botStore.activeBot.selectedBacktestResult.strategy_name;
|
||||
botStore.activeBot.getStrategy(strategy.value);
|
||||
selectedTimeframe.value = botStore.activeBot.selectedBacktestResult.timeframe;
|
||||
selectedDetailTimeframe.value = botStore.activeBot.selectedBacktestResult.timeframe_detail || '';
|
||||
btStore.strategy = botStore.activeBot.selectedBacktestResult.strategy_name;
|
||||
botStore.activeBot.getStrategy(btStore.strategy);
|
||||
btStore.selectedTimeframe = botStore.activeBot.selectedBacktestResult.timeframe;
|
||||
btStore.selectedDetailTimeframe =
|
||||
botStore.activeBot.selectedBacktestResult.timeframe_detail || '';
|
||||
// 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(
|
||||
|
@ -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());
|
||||
watch(
|
||||
() => botStore.activeBot.backtestRunning,
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||
:col-num="12"
|
||||
@layout-updated="layoutUpdatedEvent"
|
||||
@breakpoint-changed="breakpointChanged"
|
||||
@update:breakpoint="breakpointChanged"
|
||||
>
|
||||
<template #default="{ gridItemProps }">
|
||||
<grid-item
|
||||
|
@ -28,7 +28,7 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer :header="`Daily Profit ${botStore.botCount > 1 ? 'combined' : ''}`">
|
||||
<DailyChart
|
||||
<TimePeriodChart
|
||||
v-if="botStore.allDailyStatsSelectedBots"
|
||||
:daily-stats="botStore.allDailyStatsSelectedBots"
|
||||
:show-title="false"
|
||||
|
@ -159,7 +159,7 @@
|
|||
<script setup lang="ts">
|
||||
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 TradesLogChart from '@/components/charts/TradesLog.vue';
|
||||
import ProfitDistributionChart from '@/components/charts/ProfitDistributionChart.vue';
|
||||
|
@ -177,8 +177,8 @@ const botStore = useBotStore();
|
|||
const layoutStore = useLayoutStore();
|
||||
const currentBreakpoint = ref('');
|
||||
|
||||
const breakpointChanged = (newBreakpoint) => {
|
||||
// // console.log('breakpoint:', newBreakpoint);
|
||||
const breakpointChanged = (newBreakpoint: string) => {
|
||||
// console.log('breakpoint:', newBreakpoint);
|
||||
currentBreakpoint.value = newBreakpoint;
|
||||
};
|
||||
const isResizableLayout = computed(() =>
|
||||
|
@ -235,7 +235,7 @@ const responsiveGridLayouts = computed(() => {
|
|||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await botStore.allGetDaily({ timescale: 30 });
|
||||
botStore.allGetDaily({ timescale: 30 });
|
||||
// botStore.activeBot.getTrades();
|
||||
botStore.activeBot.getOpenTrades();
|
||||
botStore.activeBot.getProfit();
|
||||
|
|
|
@ -45,12 +45,12 @@
|
|||
<b-tab title="Balance" lazy>
|
||||
<Balance />
|
||||
</b-tab>
|
||||
<b-tab title="Daily Stats" lazy>
|
||||
<DailyStats />
|
||||
<b-tab title="Time Breakdown" lazy>
|
||||
<PeriodBreakdown />
|
||||
</b-tab>
|
||||
|
||||
<b-tab title="Pairlist" lazy>
|
||||
<FTBotAPIPairList />
|
||||
<PairListLive />
|
||||
</b-tab>
|
||||
<b-tab title="Pair Locks" lazy>
|
||||
<PairLockList />
|
||||
|
@ -152,9 +152,9 @@ import Balance from '@/components/ftbot/BotBalance.vue';
|
|||
import BotControls from '@/components/ftbot/BotControls.vue';
|
||||
import BotStatus from '@/components/ftbot/BotStatus.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 FTBotAPIPairList from '@/components/ftbot/FTBotAPIPairList.vue';
|
||||
import PairListLive from '@/components/ftbot/PairListLive.vue';
|
||||
import PairLockList from '@/components/ftbot/PairLockList.vue';
|
||||
import PairSummary from '@/components/ftbot/PairSummary.vue';
|
||||
import BotPerformance from '@/components/ftbot/BotPerformance.vue';
|
||||
|
@ -169,7 +169,7 @@ const botStore = useBotStore();
|
|||
const layoutStore = useLayoutStore();
|
||||
const currentBreakpoint = ref('');
|
||||
|
||||
const breakpointChanged = (newBreakpoint) => {
|
||||
const breakpointChanged = (newBreakpoint: string) => {
|
||||
// console.log('breakpoint:', newBreakpoint);
|
||||
currentBreakpoint.value = newBreakpoint;
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
dateStringToTimeRange,
|
||||
timestampHour,
|
||||
dateFromString,
|
||||
timestampmsOrNa,
|
||||
} from '@/shared/formatters';
|
||||
|
||||
const { getTimeZone } = exportForTesting;
|
||||
|
@ -31,6 +32,8 @@ describe('timeformatter.ts', () => {
|
|||
expect(timestampmsWithTimezone(1651057500000)).toEqual('2022-04-27 11:05:00 (UTC)');
|
||||
setTimezone('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', () => {
|
||||
setTimezone('UTC');
|
||||
|
@ -38,6 +41,16 @@ describe('timeformatter.ts', () => {
|
|||
setTimezone('CET');
|
||||
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', () => {
|
||||
expect(timestampToDateString(1651057500000)).toEqual('2022-04-27');
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user