diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index d21d0a2..68320c9 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -103,6 +103,18 @@ public class BacktestController : BaseController return Ok(_backtester.DeleteBacktestByUser(user, id)); } + /// + /// Deletes multiple backtests by their IDs for the authenticated user. + /// + /// The request containing the array of backtest IDs to delete. + /// An ActionResult indicating the outcome of the operation. + [HttpDelete("multiple")] + public async Task DeleteBacktests([FromBody] DeleteBacktestsRequest request) + { + var user = await GetUser(); + return Ok(_backtester.DeleteBacktestsByIdsForUser(user, request.BacktestIds)); + } + /// /// Retrieves all backtests for a specific genetic request ID. /// This endpoint is used to view the results of a genetic algorithm optimization. diff --git a/src/Managing.Api/Models/Requests/DeleteBacktestsRequest.cs b/src/Managing.Api/Models/Requests/DeleteBacktestsRequest.cs new file mode 100644 index 0000000..836c0d6 --- /dev/null +++ b/src/Managing.Api/Models/Requests/DeleteBacktestsRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Managing.Api.Models.Requests; + +/// +/// Request model for deleting multiple backtests by their IDs +/// +public class DeleteBacktestsRequest +{ + /// + /// Array of backtest IDs to delete + /// + [Required] + [MinLength(1, ErrorMessage = "At least one backtest ID must be provided")] + public string[] BacktestIds { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs index c3873b7..90e4ad0 100644 --- a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs @@ -10,5 +10,6 @@ public interface IBacktestRepository IEnumerable GetBacktestsByRequestId(string requestId); Backtest GetBacktestByIdForUser(User user, string id); void DeleteBacktestByIdForUser(User user, string id); + void DeleteBacktestsByIdsForUser(User user, IEnumerable ids); void DeleteAllBacktestsForUser(User user); } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index 6fb0a47..35b7a82 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -56,6 +56,7 @@ namespace Managing.Application.Abstractions.Services IEnumerable GetBacktestsByRequestId(string requestId); Backtest GetBacktestByIdForUser(User user, string id); bool DeleteBacktestByUser(User user, string id); + bool DeleteBacktestsByIdsForUser(User user, IEnumerable ids); bool DeleteBacktestsByUser(User user); diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index c554d64..9cfbebd 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -143,8 +143,7 @@ namespace Managing.Application.Backtesting tradingBot.User = user; await tradingBot.LoadAccount(); - var result = - await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata); + var result = await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata); if (user != null) { @@ -297,25 +296,8 @@ namespace Managing.Application.Backtesting { try { - // Check if backtest meets criteria: score > 85, at least 5 positions, winrate > 65%, risk-reward >= 1.5:1 - var score = backtest.Score; - var tradeCount = backtest.Positions?.Count ?? 0; - var winRate = backtest.WinRate; - - // Calculate risk-reward ratio from money management settings - var riskRewardRatio = 0.0; - if (backtest.Config.MoneyManagement != null) - { - var stopLoss = (double)backtest.Config.MoneyManagement.StopLoss; - var takeProfit = (double)backtest.Config.MoneyManagement.TakeProfit; - - if (stopLoss > 0 && takeProfit > 0) - { - riskRewardRatio = takeProfit / stopLoss; - } - } - - if (score > 85 && tradeCount >= 5 && winRate > 65 && riskRewardRatio >= 1.5) + + if (backtest.Score > 80) { await _messengerService.SendBacktestNotification(backtest); } @@ -466,6 +448,20 @@ namespace Managing.Application.Backtesting } } + public bool DeleteBacktestsByIdsForUser(User user, IEnumerable ids) + { + try + { + _backtestRepository.DeleteBacktestsByIdsForUser(user, ids); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete backtests for user {UserName}", user.Name); + return false; + } + } + public bool DeleteBacktestsByUser(User user) { try diff --git a/src/Managing.Infrastructure.Database/BacktestRepository.cs b/src/Managing.Infrastructure.Database/BacktestRepository.cs index eda9a55..e615bd3 100644 --- a/src/Managing.Infrastructure.Database/BacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/BacktestRepository.cs @@ -64,6 +64,18 @@ public class BacktestRepository : IBacktestRepository } } + public void DeleteBacktestsByIdsForUser(User user, IEnumerable ids) + { + var backtests = _backtestRepository.AsQueryable() + .Where(b => b.User != null && b.User.Name == user.Name && ids.Contains(b.Identifier)) + .ToList(); + + foreach (var backtest in backtests) + { + _backtestRepository.DeleteById(backtest.Id.ToString()); + } + } + public void DeleteAllBacktestsForUser(User user) { var backtests = _backtestRepository.AsQueryable() diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 5c2523f..758122f 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -498,6 +498,50 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + backtest_DeleteBacktests(request: DeleteBacktestsRequest): Promise { + let url_ = this.baseUrl + "/Backtest/multiple"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(request); + + let options_: RequestInit = { + body: content_, + method: "DELETE", + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_DeleteBacktests(_response); + }); + } + + protected processBacktest_DeleteBacktests(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + backtest_GetBacktestsByRequestId(requestId: string): Promise { let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}"; if (requestId === undefined || requestId === null) @@ -3648,6 +3692,10 @@ export interface SuperTrendResult extends ResultBase { lowerBand?: number | null; } +export interface DeleteBacktestsRequest { + backtestIds: string[]; +} + export interface RunBacktestRequest { config?: TradingBotConfigRequest | null; startDate?: Date; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 5e477c0..4947818 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -595,6 +595,10 @@ export interface SuperTrendResult extends ResultBase { lowerBand?: number | null; } +export interface DeleteBacktestsRequest { + backtestIds: string[]; +} + export interface RunBacktestRequest { config?: TradingBotConfigRequest | null; startDate?: Date; diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx index 4b75e88..c06569a 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx @@ -5,7 +5,7 @@ import React, {useEffect, useState} from 'react' import 'react-toastify/dist/ReactToastify.css' import useApiUrlStore from '../../app/store/apiStore' import useBacktestStore from '../../app/store/backtestStore' -import {Loader} from '../../components/atoms' +import {Loader, Slider} from '../../components/atoms' import {Modal, Toast} from '../../components/mollecules' import {BacktestTable, UnifiedTradingModal} from '../../components/organism' import {BacktestClient} from '../../generated/ManagingApi' @@ -13,6 +13,11 @@ import {BacktestClient} from '../../generated/ManagingApi' const BacktestScanner: React.FC = () => { const [showModal, setShowModal] = useState(false) const [showModalRemoveBacktest, setShowModalRemoveBacktest] = useState(false) + const [filteredCount, setFilteredCount] = useState(0) + const [filterValues, setFilterValues] = useState({ + winRate: 50, + score: 50 + }) const { apiUrl } = useApiUrlStore() const { backtests: backtestingResult, setBacktests, setLoading } = useBacktestStore() const client = new BacktestClient({}, apiUrl) @@ -32,8 +37,55 @@ const BacktestScanner: React.FC = () => { setLoading(isLoading) }, [isLoading, setLoading]) + useEffect(() => { + if (backtestingResult && showModalRemoveBacktest) { + calculateFilteredCount() + } + }, [backtestingResult, showModalRemoveBacktest]) + const openModalRemoveBacktests = () => { setShowModalRemoveBacktest(true) + // Calculate initial filtered count + calculateFilteredCount() + } + + const calculateFilteredCount = (formData?: any) => { + if (!backtestingResult) { + setFilteredCount(0) + return + } + + const filters = formData || filterValues + + const filteredBacktests = backtestingResult.filter((backtest: any) => { + // Ensure values are numbers and handle potential null/undefined values + const backtestWinRate = Number(backtest.winRate) || 0 + const backtestScore = Number(backtest.score) || 0 + + // Debug logging to check the data structure + console.log('Backtest:', { + id: backtest.id, + winRate: backtest.winRate, + winRateNumber: backtestWinRate, + score: backtest.score, + scoreNumber: backtestScore, + filters: filters + }) + + return ( + backtestWinRate <= filters.winRate && + backtestScore <= filters.score + ) + }) + + console.log('Filtered count:', filteredBacktests.length, 'Total:', backtestingResult.length) + setFilteredCount(filteredBacktests.length) + } + + const updateFilterValue = (field: string, value: number) => { + const newValues = { ...filterValues, [field]: value } + setFilterValues(newValues) + calculateFilteredCount(newValues) } const closeModalRemoveBacktest = () => { @@ -44,12 +96,11 @@ const BacktestScanner: React.FC = () => { event.preventDefault() const form = { - finalPnl: Number(event.target.finalPnl.value), - hp: Number(event.target.hp.value), - winRate: Number(event.target.winRate.value), + winRate: filterValues.winRate, + score: filterValues.score, } + const notify = new Toast(`Deleting Backtests...`) - const client = new BacktestClient({}, apiUrl) closeModalRemoveBacktest() if (!backtestingResult) { @@ -57,22 +108,31 @@ const BacktestScanner: React.FC = () => { } const backTestToDelete = backtestingResult.filter((backtest: any) => { - const H_P = backtest.growthPercentage - backtest.hodlPercentage + // Ensure values are numbers and handle potential null/undefined values + const backtestWinRate = Number(backtest.winRate) || 0 + const backtestScore = Number(backtest.score) || 0 + return ( - backtest.winRate <= form.winRate && - backtest.finalPnl <= form.finalPnl && - H_P <= form.hp + backtestWinRate <= form.winRate && + backtestScore <= form.score ) }) - backTestToDelete.forEach(async (backtest) => { - return await client - .backtest_DeleteBacktest(backtest.id) - .then(() => {}) - .catch((err) => { - notify.update('error', err) - }) - }) - notify.update('success', 'Backtest deleted') + + if (backTestToDelete.length === 0) { + notify.update('warning', 'No backtests match the criteria') + return + } + + try { + const backtestIds = backTestToDelete.map((backtest: any) => backtest.id) + await client.backtest_DeleteBacktests({ backtestIds }) + notify.update('success', `${backTestToDelete.length} backtests deleted successfully`) + + // Refetch backtests to update the list + refetch() + } catch (err: any) { + notify.update('error', err?.message || 'An error occurred while deleting backtests') + } } function openModal() { @@ -118,56 +178,76 @@ const BacktestScanner: React.FC = () => { showModal={showModalRemoveBacktest} onSubmit={onSubmitRemoveBacktest} onClose={closeModalRemoveBacktest} - titleHeader={'Remove Backtest'} + titleHeader={'Delete Backtests by Filters'} > - - - - PnL{'<'} - - - - - - - WinRate{'<'} + WinRate ≤ - + updateFilterValue('winRate', Number(e.target.value))} /> - + ) => updateFilterValue('winRate', Number(e.target.value))} + min={0} + max={100} + step={1} + prefixValue="" + suffixValue="%" + /> + + - - H/P{'<'} + + Score ≤ - + updateFilterValue('score', Number(e.target.value))} /> - + ) => updateFilterValue('score', Number(e.target.value))} + min={0} + max={100} + step={1} + prefixValue="" + suffixValue="" + /> + + + + + + + + + This will delete {filteredCount} backtest{filteredCount !== 1 ? 's' : ''} that match the criteria - - Run + + Delete {filteredCount} Backtest{filteredCount !== 1 ? 's' : ''}