mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-13 03:33:50 +00:00
Merge pull request #943 from freqtrade/new_charts
Add profit chart and closed trades section
This commit is contained in:
commit
2d6dc674a2
151
src/components/charts/ProfitDistributionChart.vue
Normal file
151
src/components/charts/ProfitDistributionChart.vue
Normal 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>
|
19
src/shared/charts/binCount.ts
Normal file
19
src/shared/charts/binCount.ts
Normal 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;
|
||||||
|
}
|
|
@ -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]) => {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
|
|
48
tests/unit/bincount.spec.ts
Normal file
48
tests/unit/bincount.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user