Merge pull request #943 from freqtrade/new_charts

Add profit chart and closed trades section
This commit is contained in:
Matthias 2022-09-14 20:29:43 +02:00 committed by GitHub
commit 2d6dc674a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 293 additions and 9 deletions

View File

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

View File

@ -0,0 +1,19 @@
export function binData(data: number[], bins: number) {
const minimum = Math.min(...data);
const maximum = Math.max(...data);
const binSize = ((maximum - minimum) * 1.01) / bins;
// console.log(`data ranges from ${minimum} to ${maximum}, binsize ${binSize}`);
// Count occurances an array with [bucketStart, count in this bucket]
const baseBins = [...Array(bins).keys()].map((i) => [
Math.round((minimum + i * binSize) * 1000) / 1000,
0,
]);
// console.log(baseBins);
for (let i = 0; i < data.length; i++) {
const index = Math.min(Math.floor((data[i] - minimum) / binSize), bins - 1);
// console.log(data[i], index)
baseBins[index][1]++;
}
return baseBins;
}

View File

@ -97,6 +97,20 @@ export const useBotStore = defineStore('wrapper', {
}); });
return result; return result;
}, },
allClosedTradesSelectedBots: (state): Trade[] => {
const result: Trade[] = [];
Object.entries(state.botStores).forEach(([, botStore]) => {
if (botStore.isSelected) {
result.push(...botStore.trades);
}
});
return result.sort((a, b) =>
// Sort by close timestamp, then by tradeid
b.close_timestamp && a.close_timestamp
? b.close_timestamp - a.close_timestamp
: b.trade_id - a.trade_id,
);
},
allTradesSelectedBots: (state): ClosedTrade[] => { allTradesSelectedBots: (state): ClosedTrade[] => {
const result: ClosedTrade[] = []; const result: ClosedTrade[] = [];
Object.entries(state.botStores).forEach(([, botStore]) => { Object.entries(state.botStores).forEach(([, botStore]) => {

View File

@ -14,6 +14,8 @@ export enum DashboardLayout {
botComparison = 'g-botComparison', botComparison = 'g-botComparison',
allOpenTrades = 'g-allOpenTrades', allOpenTrades = 'g-allOpenTrades',
cumChartChart = 'g-cumChartChart', cumChartChart = 'g-cumChartChart',
allClosedTrades = 'g-allClosedTrades',
profitDistributionChart = 'g-profitDistributionChart',
tradesLogChart = 'g-TradesLogChart', tradesLogChart = 'g-TradesLogChart',
} }
@ -40,7 +42,9 @@ const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
{ i: DashboardLayout.dailyChart, x: 8, y: 0, w: 4, h: 6 }, { i: DashboardLayout.dailyChart, x: 8, y: 0, w: 4, h: 6 },
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 8, h: 6 }, { i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 8, h: 6 },
{ i: DashboardLayout.cumChartChart, x: 8, y: 6, w: 4, h: 6 }, { i: DashboardLayout.cumChartChart, x: 8, y: 6, w: 4, h: 6 },
{ i: DashboardLayout.tradesLogChart, x: 0, y: 12, w: 12, h: 4 }, { i: DashboardLayout.allClosedTrades, x: 0, y: 12, w: 8, h: 6 },
{ i: DashboardLayout.profitDistributionChart, x: 8, y: 12, w: 4, h: 6 },
{ i: DashboardLayout.tradesLogChart, x: 0, y: 18, w: 12, h: 4 },
]; ];
const DEFAULT_DASHBOARD_LAYOUT_SM: GridItemData[] = [ const DEFAULT_DASHBOARD_LAYOUT_SM: GridItemData[] = [
@ -48,7 +52,9 @@ const DEFAULT_DASHBOARD_LAYOUT_SM: GridItemData[] = [
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 12, h: 8 }, { i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 12, h: 8 },
{ i: DashboardLayout.dailyChart, x: 0, y: 14, w: 12, h: 6 }, { i: DashboardLayout.dailyChart, x: 0, y: 14, w: 12, h: 6 },
{ i: DashboardLayout.cumChartChart, x: 0, y: 20, w: 12, h: 6 }, { i: DashboardLayout.cumChartChart, x: 0, y: 20, w: 12, h: 6 },
{ i: DashboardLayout.tradesLogChart, x: 0, y: 26, w: 12, h: 4 }, { i: DashboardLayout.profitDistributionChart, x: 0, y: 26, w: 12, h: 6 },
{ i: DashboardLayout.tradesLogChart, x: 0, y: 32, w: 12, h: 4 },
{ i: DashboardLayout.allClosedTrades, x: 0, y: 36, w: 12, h: 8 },
]; ];
const STORE_LAYOUTS = 'ftLayoutSettings'; const STORE_LAYOUTS = 'ftLayoutSettings';

View File

@ -265,6 +265,12 @@
class="cum-profit" class="cum-profit"
:show-title="true" :show-title="true"
/> />
<hr />
<ProfitDistributionChart
class="mt-3"
:trades="botStore.activeBot.selectedBacktestResult.trades"
:show-title="true"
/>
</div> </div>
</div> </div>
@ -349,6 +355,7 @@ import TimeframeSelect from '@/components/ftbot/TimeframeSelect.vue';
import TradeList from '@/components/ftbot/TradeList.vue'; import TradeList from '@/components/ftbot/TradeList.vue';
import TradeListNav from '@/components/ftbot/TradeListNav.vue'; import TradeListNav from '@/components/ftbot/TradeListNav.vue';
import BacktestHistoryLoad from '@/components/ftbot/BacktestHistoryLoad.vue'; import BacktestHistoryLoad from '@/components/ftbot/BacktestHistoryLoad.vue';
import ProfitDistributionChart from '@/components/charts/ProfitDistributionChart.vue';
import { BacktestPayload, ChartSliderPosition, Trade } from '@/types'; import { BacktestPayload, ChartSliderPosition, Trade } from '@/types';
@ -366,6 +373,7 @@ export default defineComponent({
CandleChartContainer, CandleChartContainer,
CumProfitChart, CumProfitChart,
TradesLogChart, TradesLogChart,
ProfitDistributionChart,
StrategySelect, StrategySelect,
PairSummary, PairSummary,
TimeframeSelect, TimeframeSelect,

View File

@ -57,11 +57,7 @@
drag-allow-from=".drag-header" drag-allow-from=".drag-header"
> >
<DraggableContainer header="Open Trades"> <DraggableContainer header="Open Trades">
<trade-list <trade-list active-trades :trades="botStore.allOpenTradesSelectedBots" multi-bot-view />
:active-trades="true"
:trades="botStore.allOpenTradesSelectedBots"
multi-bot-view
/>
</DraggableContainer> </DraggableContainer>
</GridItem> </GridItem>
<GridItem <GridItem
@ -78,6 +74,39 @@
<CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" /> <CumProfitChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer> </DraggableContainer>
</GridItem> </GridItem>
<GridItem
:i="gridLayoutAllClosedTrades.i"
:x="gridLayoutAllClosedTrades.x"
:y="gridLayoutAllClosedTrades.y"
:w="gridLayoutAllClosedTrades.w"
:h="gridLayoutAllClosedTrades.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Closed Trades">
<trade-list
:active-trades="false"
show-filter
:trades="botStore.allClosedTradesSelectedBots"
multi-bot-view
/>
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutProfitDistribution.i"
:x="gridLayoutProfitDistribution.x"
:y="gridLayoutProfitDistribution.y"
:w="gridLayoutProfitDistribution.w"
:h="gridLayoutProfitDistribution.h"
:min-w="3"
:min-h="4"
drag-allow-from=".drag-header"
>
<DraggableContainer header="Profit Distribution">
<ProfitDistributionChart :trades="botStore.allTradesSelectedBots" :show-title="false" />
</DraggableContainer>
</GridItem>
<GridItem <GridItem
:i="gridLayoutTradesLogChart.i" :i="gridLayoutTradesLogChart.i"
:x="gridLayoutTradesLogChart.x" :x="gridLayoutTradesLogChart.x"
@ -103,6 +132,7 @@ import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
import DailyChart from '@/components/charts/DailyChart.vue'; import DailyChart from '@/components/charts/DailyChart.vue';
import CumProfitChart from '@/components/charts/CumProfitChart.vue'; import CumProfitChart from '@/components/charts/CumProfitChart.vue';
import TradesLogChart from '@/components/charts/TradesLog.vue'; import TradesLogChart from '@/components/charts/TradesLog.vue';
import ProfitDistributionChart from '@/components/charts/ProfitDistributionChart.vue';
import BotComparisonList from '@/components/ftbot/BotComparisonList.vue'; import BotComparisonList from '@/components/ftbot/BotComparisonList.vue';
import TradeList from '@/components/ftbot/TradeList.vue'; import TradeList from '@/components/ftbot/TradeList.vue';
import DraggableContainer from '@/components/layout/DraggableContainer.vue'; import DraggableContainer from '@/components/layout/DraggableContainer.vue';
@ -118,6 +148,7 @@ export default defineComponent({
GridItem, GridItem,
DailyChart, DailyChart,
CumProfitChart, CumProfitChart,
ProfitDistributionChart,
TradesLogChart, TradesLogChart,
BotComparisonList, BotComparisonList,
TradeList, TradeList,
@ -166,11 +197,16 @@ export default defineComponent({
const gridLayoutAllOpenTrades = computed((): GridItemData => { const gridLayoutAllOpenTrades = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.allOpenTrades); return findGridLayout(gridLayout.value, DashboardLayout.allOpenTrades);
}); });
const gridLayoutAllClosedTrades = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.allClosedTrades);
});
const gridLayoutCumChart = computed((): GridItemData => { const gridLayoutCumChart = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.cumChartChart); return findGridLayout(gridLayout.value, DashboardLayout.cumChartChart);
}); });
const gridLayoutProfitDistribution = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.profitDistributionChart);
});
const gridLayoutTradesLogChart = computed((): GridItemData => { const gridLayoutTradesLogChart = computed((): GridItemData => {
return findGridLayout(gridLayout.value, DashboardLayout.tradesLogChart); return findGridLayout(gridLayout.value, DashboardLayout.tradesLogChart);
}); });
@ -198,7 +234,9 @@ export default defineComponent({
gridLayoutDaily, gridLayoutDaily,
gridLayoutBotComparison, gridLayoutBotComparison,
gridLayoutAllOpenTrades, gridLayoutAllOpenTrades,
gridLayoutAllClosedTrades,
gridLayoutCumChart, gridLayoutCumChart,
gridLayoutProfitDistribution,
gridLayoutTradesLogChart, gridLayoutTradesLogChart,
responsiveGridLayouts, responsiveGridLayouts,
}; };

View File

@ -85,7 +85,7 @@
drag-allow-from=".card-header" drag-allow-from=".card-header"
> >
<DraggableContainer header="Closed Trades"> <DraggableContainer header="Closed Trades">
<TradeList <trade-list
class="trade-history" class="trade-history"
:trades="botStore.activeBot.closedTrades" :trades="botStore.activeBot.closedTrades"
title="Trade history" title="Trade history"

View File

@ -0,0 +1,48 @@
import { binData } from '@/shared/charts/binCount';
describe('binCount.ts', () => {
it('Bins data as expected', () => {
const testData = [1, 1, 2, 3, 5, 6, 8, 10];
const res = binData(testData, 3);
expect(res.length).toEqual(3);
expect(res).toEqual([
[1, 4],
[4.03, 2],
[7.06, 2],
]);
expect(res.map((v) => v[1]).reduce((a, b) => a + b)).toEqual(testData.length);
const res1 = binData(testData, 5);
// expect(res1.length).toEqual(5);
expect(res1).toEqual([
[1, 3],
[2.818, 1],
[4.636, 2],
[6.454, 1],
[8.272, 1],
]);
expect(res1.map((v) => v[1]).reduce((a, b) => a + b)).toEqual(testData.length);
});
it('Bins data with negatives', () => {
const testData = [1, 1, 2, 3, 5, 6, 8, -1, -3, -5, -4];
const res = binData(testData, 3);
expect(res.length).toEqual(3);
expect(res.map((v) => v[1]).reduce((a, b) => a + b)).toEqual(testData.length);
expect(res).toEqual([
[-5, 4],
[-0.623, 4],
[3.753, 3],
]);
});
it('Bins data performant', () => {
const randomSize = 20000;
const randomData = Array.from({ length: randomSize }, () => Math.floor(Math.random() * 10));
const startTime = Date.now();
const res = binData(randomData, 5);
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(20);
expect(res.map((v) => v[1]).reduce((a, b) => a + b)).toEqual(randomData.length);
});
});