Add filters and sorting for backtests

This commit is contained in:
2025-10-14 18:06:36 +07:00
parent 49b0f7b696
commit 74adad5834
21 changed files with 4028 additions and 81 deletions

View File

@@ -22,6 +22,7 @@
"@privy-io/wagmi": "^1.0.3",
"@tailwindcss/typography": "^0.5.0",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.3",
"@wagmi/chains": "^0.2.9",
"@wagmi/connectors": "^5.7.3",
"@wagmi/core": "^2.17.0",

View File

@@ -5,7 +5,7 @@ import {useExpanded, useFilters, usePagination, useSortBy, useTable,} from 'reac
import useApiUrlStore from '../../../app/store/apiStore'
import useBacktestStore from '../../../app/store/backtestStore'
import type {Backtest, LightBacktestResponse} from '../../../generated/ManagingApi'
import {BacktestClient} from '../../../generated/ManagingApi'
import {BacktestClient, BacktestSortableColumn, IndicatorType} from '../../../generated/ManagingApi'
import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter} from '../../mollecules'
import {UnifiedTradingModal} from '../index'
import Toast from '../../mollecules/Toast/Toast'
@@ -72,15 +72,15 @@ const ServerSortableTable = ({
</p>
<span className="relative">
{(() => {
// Map backend field names to column IDs for comparison
const backendToColumnMapping: { [key: string]: string } = {
'score': 'score',
'finalpnl': 'finalPnl',
'winrate': 'winRate',
'growthpercentage': 'growthPercentage',
'hodlpercentage': 'hodlPercentage',
// Map enum sortable fields to table column ids
const enumToColumnMapping: { [key in BacktestSortableColumn]?: string } = {
[BacktestSortableColumn.Score]: 'score',
[BacktestSortableColumn.FinalPnl]: 'finalPnl',
[BacktestSortableColumn.WinRate]: 'winRate',
[BacktestSortableColumn.GrowthPercentage]: 'growthPercentage',
[BacktestSortableColumn.HodlPercentage]: 'hodlPercentage',
};
const columnId = backendToColumnMapping[currentSort?.sortBy || ''] || currentSort?.sortBy;
const columnId = enumToColumnMapping[currentSort?.sortBy as BacktestSortableColumn] || currentSort?.sortBy;
return currentSort?.sortBy && columnId === column.id ? (
currentSort.sortOrder === 'desc' ? (
@@ -133,14 +133,36 @@ interface BacktestTableProps {
list: LightBacktestResponse[] | undefined
isFetching?: boolean
displaySummary?: boolean
onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void
currentSort?: { sortBy: string; sortOrder: 'asc' | 'desc' }
onSortChange?: (sortBy: BacktestSortableColumn, sortOrder: 'asc' | 'desc') => void
currentSort?: { sortBy: BacktestSortableColumn; sortOrder: 'asc' | 'desc' }
onBacktestDeleted?: () => void // Callback when a backtest is deleted
onFiltersChange?: (filters: {
scoreMin?: number | null
scoreMax?: number | null
winrateMin?: number | null
winrateMax?: number | null
maxDrawdownMax?: number | null
tickers?: string[] | null
indicators?: string[] | null
durationMinDays?: number | null
durationMaxDays?: number | null
}) => void
filters?: {
scoreMin?: number | null
scoreMax?: number | null
winrateMin?: number | null
winrateMax?: number | null
maxDrawdownMax?: number | null
tickers?: string[] | null
indicators?: string[] | null
durationMinDays?: number | null
durationMaxDays?: number | null
}
}
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted}) => {
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted, onFiltersChange, filters}) => {
const [rows, setRows] = useState<LightBacktestResponse[]>([])
const {apiUrl} = useApiUrlStore()
const {removeBacktest} = useBacktestStore()
@@ -157,20 +179,75 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
const [showConfigDisplayModal, setShowConfigDisplayModal] = useState(false)
const [selectedBacktestForConfigView, setSelectedBacktestForConfigView] = useState<Backtest | null>(null)
// Filters sidebar state
const [isFilterOpen, setIsFilterOpen] = useState(false)
const [scoreMin, setScoreMin] = useState<number>(0)
const [scoreMax, setScoreMax] = useState<number>(100)
const [winMin, setWinMin] = useState<number>(0)
const [winMax, setWinMax] = useState<number>(100)
const [maxDrawdownMax, setMaxDrawdownMax] = useState<number | ''>('')
const [tickersInput, setTickersInput] = useState<string>('')
const [selectedIndicators, setSelectedIndicators] = useState<string[]>([])
const [durationMinDays, setDurationMinDays] = useState<number | null>(null)
const [durationMaxDays, setDurationMaxDays] = useState<number | null>(null)
const applyFilters = () => {
if (!onFiltersChange) return
onFiltersChange({
scoreMin,
scoreMax,
winrateMin: winMin,
winrateMax: winMax,
maxDrawdownMax: maxDrawdownMax === '' ? null : Number(maxDrawdownMax),
tickers: tickersInput ? tickersInput.split(',').map(s => s.trim()).filter(Boolean) : null,
indicators: selectedIndicators.length ? selectedIndicators : null,
durationMinDays,
durationMaxDays,
})
setIsFilterOpen(false)
}
const clearDuration = () => {
setDurationMinDays(null)
setDurationMaxDays(null)
}
const toggleIndicator = (name: string) => {
setSelectedIndicators(prev => prev.includes(name)
? prev.filter(i => i !== name)
: [...prev, name])
}
const clearIndicators = () => setSelectedIndicators([])
// Sync incoming filters prop to local sidebar state
useEffect(() => {
if (!filters) return
if (typeof filters.scoreMin === 'number') setScoreMin(filters.scoreMin)
if (typeof filters.scoreMax === 'number') setScoreMax(filters.scoreMax)
if (typeof filters.winrateMin === 'number') setWinMin(filters.winrateMin)
if (typeof filters.winrateMax === 'number') setWinMax(filters.winrateMax)
if (typeof filters.maxDrawdownMax === 'number') setMaxDrawdownMax(filters.maxDrawdownMax)
setTickersInput(filters.tickers && filters.tickers.length ? filters.tickers.join(',') : '')
setSelectedIndicators(filters.indicators ? [...filters.indicators] : [])
setDurationMinDays(filters.durationMinDays ?? null)
setDurationMaxDays(filters.durationMaxDays ?? null)
}, [filters])
// Handle sort change
const handleSortChange = (columnId: string, sortOrder: 'asc' | 'desc') => {
if (!onSortChange) return;
// Map column IDs to backend field names
const sortByMapping: { [key: string]: string } = {
'score': 'score',
'finalPnl': 'finalpnl',
'winRate': 'winrate',
'growthPercentage': 'growthpercentage',
'hodlPercentage': 'hodlpercentage',
// Map column IDs to BacktestSortableColumn enum
const sortByMapping: { [key: string]: BacktestSortableColumn } = {
'score': BacktestSortableColumn.Score,
'finalPnl': BacktestSortableColumn.FinalPnl,
'winRate': BacktestSortableColumn.WinRate,
'growthPercentage': BacktestSortableColumn.GrowthPercentage,
'hodlPercentage': BacktestSortableColumn.HodlPercentage,
};
const backendSortBy = sortByMapping[columnId] || 'score';
const backendSortBy = sortByMapping[columnId] || BacktestSortableColumn.Score;
onSortChange(backendSortBy, sortOrder);
};
@@ -486,6 +563,11 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
</div>
) : (
<>
{/* Filters toggle button */}
<div className="flex w-full justify-end">
<button className="btn btn-sm btn-outline" onClick={() => setIsFilterOpen(true)}>Filters</button>
</div>
<ServerSortableTable
columns={columns}
data={rows}
@@ -498,6 +580,100 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortCh
currentSort={currentSort}
/>
{/* Right sidebar filter panel */}
{isFilterOpen && (
<>
<div className="fixed inset-0 bg-black/30" onClick={() => setIsFilterOpen(false)}></div>
<div className="fixed right-0 top-0 h-full w-96 bg-base-200 shadow-xl p-4 overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Filters</h3>
<button className="btn btn-ghost btn-sm" onClick={() => setIsFilterOpen(false)}></button>
</div>
{/* Score range */}
<div className="mb-6">
<div className="mb-2 font-medium">Score between</div>
<div className="flex gap-2 items-center">
<input type="number" min={0} max={100} className="input input-bordered input-sm w-20" value={scoreMin} onChange={e => setScoreMin(Math.min(100, Math.max(0, Number(e.target.value))))} />
<span>to</span>
<input type="number" min={0} max={100} className="input input-bordered input-sm w-20" value={scoreMax} onChange={e => setScoreMax(Math.min(100, Math.max(0, Number(e.target.value))))} />
</div>
</div>
{/* Tickers */}
<div className="mb-6">
<div className="mb-2 font-medium">Tickers</div>
<input
type="text"
placeholder="e.g. BTC, ETH, SOL"
className="input input-bordered input-sm w-full"
value={tickersInput}
onChange={e => setTickersInput(e.target.value)}
/>
</div>
{/* Max Drawdown */}
<div className="mb-6">
<div className="mb-2 font-medium">Max Drawdown (max)</div>
<input type="number" className="input input-bordered input-sm w-full" placeholder="Put the max amount" value={maxDrawdownMax} onChange={e => {
const v = e.target.value
if (v === '') setMaxDrawdownMax('')
else setMaxDrawdownMax(Number(v))
}} />
</div>
{/* Indicators (enum selection) */}
<div className="mb-6">
<div className="mb-2 font-medium flex items-center justify-between">
<span>Indicators</span>
{selectedIndicators.length > 0 && (
<button className="btn btn-ghost btn-xs" onClick={clearIndicators}>Clear</button>
)}
</div>
<div className="flex flex-wrap gap-2">
{Object.values(IndicatorType).map((ind) => (
<button
key={ind as string}
className={`btn btn-xs ${selectedIndicators.includes(ind as string) ? 'btn-primary' : 'btn-outline'}`}
onClick={() => toggleIndicator(ind as string)}
>
{ind as string}
</button>
))}
</div>
</div>
{/* Winrate */}
<div className="mb-6">
<div className="mb-2 font-medium">Winrate between</div>
<div className="flex gap-2 items-center">
<input type="number" min={0} max={100} className="input input-bordered input-sm w-20" value={winMin} onChange={e => setWinMin(Math.min(100, Math.max(0, Number(e.target.value))))} />
<span>to</span>
<input type="number" min={0} max={100} className="input input-bordered input-sm w-20" value={winMax} onChange={e => setWinMax(Math.min(100, Math.max(0, Number(e.target.value))))} />
</div>
</div>
{/* Duration */}
<div className="mb-6">
<div className="mb-2 font-medium">Duration</div>
<div className="flex flex-wrap gap-2">
<button className={`btn btn-xs ${durationMaxDays === 30 && durationMinDays === 0 ? 'btn-primary' : 'btn-outline'}`} onClick={() => { setDurationMinDays(0); setDurationMaxDays(30); }}>Up to 1 month</button>
<button className={`btn btn-xs ${durationMinDays === 30 && durationMaxDays === 60 ? 'btn-primary' : 'btn-outline'}`} onClick={() => { setDurationMinDays(30); setDurationMaxDays(60); }}>1-2 months</button>
<button className={`btn btn-xs ${durationMinDays === 90 && durationMaxDays === 150 ? 'btn-primary' : 'btn-outline'}`} onClick={() => { setDurationMinDays(90); setDurationMaxDays(150); }}>3-5 months</button>
<button className={`btn btn-xs ${durationMinDays === 180 && durationMaxDays === 330 ? 'btn-primary' : 'btn-outline'}`} onClick={() => { setDurationMinDays(180); setDurationMaxDays(330); }}>6-11 months</button>
<button className={`btn btn-xs ${durationMinDays === 365 && durationMaxDays === null ? 'btn-primary' : 'btn-outline'}`} onClick={() => { setDurationMinDays(365); setDurationMaxDays(null); }}>1 year or more</button>
<button className="btn btn-xs btn-ghost" onClick={clearDuration}>Clear</button>
</div>
</div>
<div className="flex gap-2 justify-end">
<button className="btn btn-ghost btn-sm" onClick={() => setIsFilterOpen(false)}>Cancel</button>
<button className="btn btn-primary btn-sm" onClick={applyFilters}>Apply</button>
</div>
</div>
</>
)}
{/* Bot Configuration Modal */}
{selectedBacktest && (
<UnifiedTradingModal

View File

@@ -665,7 +665,7 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<PaginatedBacktestsResponse>(null as any);
}
backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined): Promise<PaginatedBacktestsResponse> {
backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: BacktestSortableColumn | undefined, sortOrder: string | null | undefined, scoreMin: number | null | undefined, scoreMax: number | null | undefined, winrateMin: number | null | undefined, winrateMax: number | null | undefined, maxDrawdownMax: number | null | undefined, tickers: string | null | undefined, indicators: string | null | undefined, durationMinDays: number | null | undefined, durationMaxDays: number | null | undefined): Promise<PaginatedBacktestsResponse> {
let url_ = this.baseUrl + "/Backtest/Paginated?";
if (page === null)
throw new Error("The parameter 'page' cannot be null.");
@@ -675,10 +675,30 @@ export class BacktestClient extends AuthorizedApiBase {
throw new Error("The parameter 'pageSize' cannot be null.");
else if (pageSize !== undefined)
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
if (sortBy !== undefined && sortBy !== null)
if (sortBy === null)
throw new Error("The parameter 'sortBy' cannot be null.");
else if (sortBy !== undefined)
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
if (sortOrder !== undefined && sortOrder !== null)
url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&";
if (scoreMin !== undefined && scoreMin !== null)
url_ += "scoreMin=" + encodeURIComponent("" + scoreMin) + "&";
if (scoreMax !== undefined && scoreMax !== null)
url_ += "scoreMax=" + encodeURIComponent("" + scoreMax) + "&";
if (winrateMin !== undefined && winrateMin !== null)
url_ += "winrateMin=" + encodeURIComponent("" + winrateMin) + "&";
if (winrateMax !== undefined && winrateMax !== null)
url_ += "winrateMax=" + encodeURIComponent("" + winrateMax) + "&";
if (maxDrawdownMax !== undefined && maxDrawdownMax !== null)
url_ += "maxDrawdownMax=" + encodeURIComponent("" + maxDrawdownMax) + "&";
if (tickers !== undefined && tickers !== null)
url_ += "tickers=" + encodeURIComponent("" + tickers) + "&";
if (indicators !== undefined && indicators !== null)
url_ += "indicators=" + encodeURIComponent("" + indicators) + "&";
if (durationMinDays !== undefined && durationMinDays !== null)
url_ += "durationMinDays=" + encodeURIComponent("" + durationMinDays) + "&";
if (durationMaxDays !== undefined && durationMaxDays !== null)
url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@@ -4275,6 +4295,21 @@ export interface LightBacktestResponse {
scoreMessage: string;
}
export enum BacktestSortableColumn {
Score = "Score",
FinalPnl = "FinalPnl",
WinRate = "WinRate",
GrowthPercentage = "GrowthPercentage",
HodlPercentage = "HodlPercentage",
Duration = "Duration",
Timeframe = "Timeframe",
IndicatorsCount = "IndicatorsCount",
MaxDrawdown = "MaxDrawdown",
Fees = "Fees",
SharpeRatio = "SharpeRatio",
Ticker = "Ticker",
}
export interface LightBacktest {
id?: string | null;
config?: TradingBotConfig | null;
@@ -4290,6 +4325,7 @@ export interface LightBacktest {
score?: number;
scoreMessage?: string | null;
metadata?: any | null;
ticker?: string | null;
}
export interface RunBacktestRequest {
@@ -4305,7 +4341,7 @@ export interface RunBacktestRequest {
}
export interface TradingBotConfigRequest {
accountName: string;
accountName?: string | null;
ticker: Ticker;
timeframe: Timeframe;
isForWatchingOnly: boolean;

View File

@@ -532,6 +532,21 @@ export interface LightBacktestResponse {
scoreMessage: string;
}
export enum BacktestSortableColumn {
Score = "Score",
FinalPnl = "FinalPnl",
WinRate = "WinRate",
GrowthPercentage = "GrowthPercentage",
HodlPercentage = "HodlPercentage",
Duration = "Duration",
Timeframe = "Timeframe",
IndicatorsCount = "IndicatorsCount",
MaxDrawdown = "MaxDrawdown",
Fees = "Fees",
SharpeRatio = "SharpeRatio",
Ticker = "Ticker",
}
export interface LightBacktest {
id?: string | null;
config?: TradingBotConfig | null;
@@ -547,6 +562,7 @@ export interface LightBacktest {
score?: number;
scoreMessage?: string | null;
metadata?: any | null;
ticker?: string | null;
}
export interface RunBacktestRequest {
@@ -562,7 +578,7 @@ export interface RunBacktestRequest {
}
export interface TradingBotConfigRequest {
accountName: string;
accountName?: string | null;
ticker: Ticker;
timeframe: Timeframe;
isForWatchingOnly: boolean;
@@ -656,7 +672,6 @@ export interface RunBundleBacktestRequest {
}
export interface BundleBacktestUniversalConfig {
accountName: string;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;

View File

@@ -8,7 +8,7 @@ import {Loader, Slider} from '../../components/atoms'
import {Modal, Toast} from '../../components/mollecules'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
import type {LightBacktestResponse} from '../../generated/ManagingApi'
import {BacktestClient} from '../../generated/ManagingApi'
import {BacktestClient, BacktestSortableColumn} from '../../generated/ManagingApi'
const PAGE_SIZE = 50
@@ -21,11 +21,24 @@ const BacktestScanner: React.FC = () => {
score: 50
})
const [currentPage, setCurrentPage] = useState(1)
const [currentSort, setCurrentSort] = useState<{ sortBy: string; sortOrder: 'asc' | 'desc' }>({
sortBy: 'score',
const [currentSort, setCurrentSort] = useState<{ sortBy: BacktestSortableColumn; sortOrder: 'asc' | 'desc' }>({
sortBy: BacktestSortableColumn.Score,
sortOrder: 'desc'
})
// Filters state coming from BacktestTable sidebar
const [filters, setFilters] = useState<{
scoreMin?: number | null
scoreMax?: number | null
winrateMin?: number | null
winrateMax?: number | null
maxDrawdownMax?: number | null
tickers?: string[] | null
indicators?: string[] | null
durationMinDays?: number | null
durationMaxDays?: number | null
}>({})
const { apiUrl } = useApiUrlStore()
const queryClient = useQueryClient()
const backtestClient = new BacktestClient({}, apiUrl)
@@ -37,13 +50,22 @@ const BacktestScanner: React.FC = () => {
error,
refetch
} = useQuery({
queryKey: ['backtests', currentPage, currentSort.sortBy, currentSort.sortOrder],
queryKey: ['backtests', currentPage, currentSort.sortBy, currentSort.sortOrder, filters],
queryFn: async () => {
const response = await backtestClient.backtest_GetBacktestsPaginated(
currentPage,
PAGE_SIZE,
currentSort.sortBy,
currentSort.sortOrder
currentPage,
PAGE_SIZE,
currentSort.sortBy,
currentSort.sortOrder,
filters.scoreMin ?? null,
filters.scoreMax ?? null,
filters.winrateMin ?? null,
filters.winrateMax ?? null,
filters.maxDrawdownMax ?? null,
(filters.tickers && filters.tickers.length ? filters.tickers.join(',') : null),
(filters.indicators && filters.indicators.length ? filters.indicators.join(',') : null),
filters.durationMinDays ?? null,
filters.durationMaxDays ?? null,
)
return {
backtests: (response.backtests as LightBacktestResponse[]) || [],
@@ -176,11 +198,27 @@ const BacktestScanner: React.FC = () => {
}
// Sorting handler
const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => {
const handleSortChange = (sortBy: BacktestSortableColumn, sortOrder: 'asc' | 'desc') => {
setCurrentSort({ sortBy, sortOrder })
setCurrentPage(1)
}
// Filters handler from BacktestTable
const handleFiltersChange = (newFilters: {
scoreMin?: number | null
scoreMax?: number | null
winrateMin?: number | null
winrateMax?: number | null
maxDrawdownMax?: number | null
tickers?: string[] | null
indicators?: string[] | null
durationMinDays?: number | null
durationMaxDays?: number | null
}) => {
setFilters(newFilters)
setCurrentPage(1)
}
// Pagination handler
const handlePageChange = (newPage: number) => {
if (newPage < 1 || (totalPages && newPage > totalPages)) return
@@ -207,11 +245,56 @@ const BacktestScanner: React.FC = () => {
</button>
</div>
{/* Selected filters summary */}
<div className="mt-2 mb-2">
{(
(filters.scoreMin !== undefined && filters.scoreMin !== null) ||
(filters.scoreMax !== undefined && filters.scoreMax !== null) ||
(filters.winrateMin !== undefined && filters.winrateMin !== null) ||
(filters.winrateMax !== undefined && filters.winrateMax !== null) ||
(filters.maxDrawdownMax !== undefined && filters.maxDrawdownMax !== null) ||
(filters.tickers && filters.tickers.length) ||
(filters.indicators && filters.indicators.length) ||
(filters.durationMinDays !== undefined && filters.durationMinDays !== null) ||
(filters.durationMaxDays !== undefined && filters.durationMaxDays !== null)
) ? (
<div className="flex flex-wrap gap-2 items-center">
<span className="text-sm opacity-70 mr-1">Active filters:</span>
{filters.scoreMin !== undefined && filters.scoreMin !== null && (
<div className="badge badge-outline">Score {filters.scoreMin}</div>
)}
{filters.scoreMax !== undefined && filters.scoreMax !== null && (
<div className="badge badge-outline">Score {filters.scoreMax}</div>
)}
{filters.winrateMin !== undefined && filters.winrateMin !== null && (
<div className="badge badge-outline">Winrate {filters.winrateMin}%</div>
)}
{filters.winrateMax !== undefined && filters.winrateMax !== null && (
<div className="badge badge-outline">Winrate {filters.winrateMax}%</div>
)}
{filters.maxDrawdownMax !== undefined && filters.maxDrawdownMax !== null && (
<div className="badge badge-outline">Max DD {filters.maxDrawdownMax}</div>
)}
{filters.tickers && filters.tickers.length > 0 && (
<div className="badge badge-outline">Tickers: {filters.tickers.join(', ')}</div>
)}
{filters.indicators && filters.indicators.length > 0 && (
<div className="badge badge-outline">Indicators: {filters.indicators.join(', ')}</div>
)}
{(filters.durationMinDays !== undefined && filters.durationMinDays !== null) || (filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) ? (
<div className="badge badge-outline">Duration: {filters.durationMinDays ?? 0}{filters.durationMaxDays ?? '∞'} days</div>
) : null}
</div>
) : null}
</div>
<BacktestTable
list={backtests as LightBacktestResponse[]} // Cast to any for backward compatibility
isFetching={isLoading}
onSortChange={handleSortChange}
currentSort={currentSort}
filters={filters}
onFiltersChange={handleFiltersChange}
onBacktestDeleted={() => {
// Invalidate backtest queries when a backtest is deleted
queryClient.invalidateQueries({ queryKey: ['backtests'] })