Delete backtest by id and with filters

This commit is contained in:
2025-07-11 22:05:46 +07:00
parent 9714da1eb9
commit 79f0cd20c1
9 changed files with 239 additions and 69 deletions

View File

@@ -103,6 +103,18 @@ public class BacktestController : BaseController
return Ok(_backtester.DeleteBacktestByUser(user, id)); return Ok(_backtester.DeleteBacktestByUser(user, id));
} }
/// <summary>
/// Deletes multiple backtests by their IDs for the authenticated user.
/// </summary>
/// <param name="request">The request containing the array of backtest IDs to delete.</param>
/// <returns>An ActionResult indicating the outcome of the operation.</returns>
[HttpDelete("multiple")]
public async Task<ActionResult> DeleteBacktests([FromBody] DeleteBacktestsRequest request)
{
var user = await GetUser();
return Ok(_backtester.DeleteBacktestsByIdsForUser(user, request.BacktestIds));
}
/// <summary> /// <summary>
/// Retrieves all backtests for a specific genetic request ID. /// Retrieves all backtests for a specific genetic request ID.
/// This endpoint is used to view the results of a genetic algorithm optimization. /// This endpoint is used to view the results of a genetic algorithm optimization.

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for deleting multiple backtests by their IDs
/// </summary>
public class DeleteBacktestsRequest
{
/// <summary>
/// Array of backtest IDs to delete
/// </summary>
[Required]
[MinLength(1, ErrorMessage = "At least one backtest ID must be provided")]
public string[] BacktestIds { get; set; } = Array.Empty<string>();
}

View File

@@ -10,5 +10,6 @@ public interface IBacktestRepository
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId); IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
Backtest GetBacktestByIdForUser(User user, string id); Backtest GetBacktestByIdForUser(User user, string id);
void DeleteBacktestByIdForUser(User user, string id); void DeleteBacktestByIdForUser(User user, string id);
void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
void DeleteAllBacktestsForUser(User user); void DeleteAllBacktestsForUser(User user);
} }

View File

@@ -56,6 +56,7 @@ namespace Managing.Application.Abstractions.Services
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId); IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
Backtest GetBacktestByIdForUser(User user, string id); Backtest GetBacktestByIdForUser(User user, string id);
bool DeleteBacktestByUser(User user, string id); bool DeleteBacktestByUser(User user, string id);
bool DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
bool DeleteBacktestsByUser(User user); bool DeleteBacktestsByUser(User user);

View File

@@ -143,8 +143,7 @@ namespace Managing.Application.Backtesting
tradingBot.User = user; tradingBot.User = user;
await tradingBot.LoadAccount(); await tradingBot.LoadAccount();
var result = var result = await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
if (user != null) if (user != null)
{ {
@@ -297,25 +296,8 @@ namespace Managing.Application.Backtesting
{ {
try 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 if (backtest.Score > 80)
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)
{ {
await _messengerService.SendBacktestNotification(backtest); await _messengerService.SendBacktestNotification(backtest);
} }
@@ -466,6 +448,20 @@ namespace Managing.Application.Backtesting
} }
} }
public bool DeleteBacktestsByIdsForUser(User user, IEnumerable<string> 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) public bool DeleteBacktestsByUser(User user)
{ {
try try

View File

@@ -64,6 +64,18 @@ public class BacktestRepository : IBacktestRepository
} }
} }
public void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> 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) public void DeleteAllBacktestsForUser(User user)
{ {
var backtests = _backtestRepository.AsQueryable() var backtests = _backtestRepository.AsQueryable()

View File

@@ -498,6 +498,50 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<Backtest>(null as any); return Promise.resolve<Backtest>(null as any);
} }
backtest_DeleteBacktests(request: DeleteBacktestsRequest): Promise<FileResponse> {
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<FileResponse> {
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<FileResponse>(null as any);
}
backtest_GetBacktestsByRequestId(requestId: string): Promise<Backtest[]> { backtest_GetBacktestsByRequestId(requestId: string): Promise<Backtest[]> {
let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}"; let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}";
if (requestId === undefined || requestId === null) if (requestId === undefined || requestId === null)
@@ -3648,6 +3692,10 @@ export interface SuperTrendResult extends ResultBase {
lowerBand?: number | null; lowerBand?: number | null;
} }
export interface DeleteBacktestsRequest {
backtestIds: string[];
}
export interface RunBacktestRequest { export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null; config?: TradingBotConfigRequest | null;
startDate?: Date; startDate?: Date;

View File

@@ -595,6 +595,10 @@ export interface SuperTrendResult extends ResultBase {
lowerBand?: number | null; lowerBand?: number | null;
} }
export interface DeleteBacktestsRequest {
backtestIds: string[];
}
export interface RunBacktestRequest { export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null; config?: TradingBotConfigRequest | null;
startDate?: Date; startDate?: Date;

View File

@@ -5,7 +5,7 @@ import React, {useEffect, useState} from 'react'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import useBacktestStore from '../../app/store/backtestStore' import useBacktestStore from '../../app/store/backtestStore'
import {Loader} from '../../components/atoms' import {Loader, Slider} from '../../components/atoms'
import {Modal, Toast} from '../../components/mollecules' import {Modal, Toast} from '../../components/mollecules'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism' import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
import {BacktestClient} from '../../generated/ManagingApi' import {BacktestClient} from '../../generated/ManagingApi'
@@ -13,6 +13,11 @@ import {BacktestClient} from '../../generated/ManagingApi'
const BacktestScanner: React.FC = () => { const BacktestScanner: React.FC = () => {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [showModalRemoveBacktest, setShowModalRemoveBacktest] = useState(false) const [showModalRemoveBacktest, setShowModalRemoveBacktest] = useState(false)
const [filteredCount, setFilteredCount] = useState(0)
const [filterValues, setFilterValues] = useState({
winRate: 50,
score: 50
})
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
const { backtests: backtestingResult, setBacktests, setLoading } = useBacktestStore() const { backtests: backtestingResult, setBacktests, setLoading } = useBacktestStore()
const client = new BacktestClient({}, apiUrl) const client = new BacktestClient({}, apiUrl)
@@ -32,8 +37,55 @@ const BacktestScanner: React.FC = () => {
setLoading(isLoading) setLoading(isLoading)
}, [isLoading, setLoading]) }, [isLoading, setLoading])
useEffect(() => {
if (backtestingResult && showModalRemoveBacktest) {
calculateFilteredCount()
}
}, [backtestingResult, showModalRemoveBacktest])
const openModalRemoveBacktests = () => { const openModalRemoveBacktests = () => {
setShowModalRemoveBacktest(true) 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 = () => { const closeModalRemoveBacktest = () => {
@@ -44,12 +96,11 @@ const BacktestScanner: React.FC = () => {
event.preventDefault() event.preventDefault()
const form = { const form = {
finalPnl: Number(event.target.finalPnl.value), winRate: filterValues.winRate,
hp: Number(event.target.hp.value), score: filterValues.score,
winRate: Number(event.target.winRate.value),
} }
const notify = new Toast(`Deleting Backtests...`) const notify = new Toast(`Deleting Backtests...`)
const client = new BacktestClient({}, apiUrl)
closeModalRemoveBacktest() closeModalRemoveBacktest()
if (!backtestingResult) { if (!backtestingResult) {
@@ -57,22 +108,31 @@ const BacktestScanner: React.FC = () => {
} }
const backTestToDelete = backtestingResult.filter((backtest: any) => { 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 ( return (
backtest.winRate <= form.winRate && backtestWinRate <= form.winRate &&
backtest.finalPnl <= form.finalPnl && backtestScore <= form.score
H_P <= form.hp
) )
}) })
backTestToDelete.forEach(async (backtest) => {
return await client if (backTestToDelete.length === 0) {
.backtest_DeleteBacktest(backtest.id) notify.update('warning', 'No backtests match the criteria')
.then(() => {}) return
.catch((err) => { }
notify.update('error', err)
}) try {
}) const backtestIds = backTestToDelete.map((backtest: any) => backtest.id)
notify.update('success', 'Backtest deleted') 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() { function openModal() {
@@ -118,56 +178,76 @@ const BacktestScanner: React.FC = () => {
showModal={showModalRemoveBacktest} showModal={showModalRemoveBacktest}
onSubmit={onSubmitRemoveBacktest} onSubmit={onSubmitRemoveBacktest}
onClose={closeModalRemoveBacktest} onClose={closeModalRemoveBacktest}
titleHeader={'Remove Backtest'} titleHeader={'Delete Backtests by Filters'}
> >
<div className="form-control">
<div className="input-group">
<label htmlFor="finalPnl" className="label mr-6">
PnL{'<'}
</label>
<label className="input-group">
<input
type="number"
className="input"
name="finalPnl"
defaultValue={0}
/>
</label>
</div>
</div>
<div className="form-control"> <div className="form-control">
<div className="input-group"> <div className="input-group">
<label htmlFor="winRate" className="label mr-6"> <label htmlFor="winRate" className="label mr-6">
WinRate{'<'} WinRate
</label> </label>
<label className="input-group"> <div className="flex items-center gap-2">
<input <input
type="number" type="number"
className="input" className="input input-sm w-20"
name="winRate" name="winRate"
defaultValue={50} value={filterValues.winRate}
min="0"
max="100"
onChange={(e) => updateFilterValue('winRate', Number(e.target.value))}
/>
<Slider
id="winRate-slider"
value={filterValues.winRate}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateFilterValue('winRate', Number(e.target.value))}
min={0}
max={100}
step={1}
prefixValue=""
suffixValue="%"
/> />
</label>
</div> </div>
</div> </div>
</div>
<div className="form-control"> <div className="form-control">
<div className="input-group"> <div className="input-group">
<label htmlFor="hp" className="label mr-6"> <label htmlFor="score" className="label mr-6">
H/P{'<'} Score
</label> </label>
<label className="input-group"> <div className="flex items-center gap-2">
<input <input
type="number" type="number"
className="input" className="input input-sm w-20"
name="hp" name="score"
defaultValue={3} value={filterValues.score}
min="0"
max={100}
onChange={(e) => updateFilterValue('score', Number(e.target.value))}
/> />
</label> <Slider
id="score-slider"
value={filterValues.score}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateFilterValue('score', Number(e.target.value))}
min={0}
max={100}
step={1}
prefixValue=""
suffixValue=""
/>
</div>
</div>
</div>
<div className="alert alert-info mb-4">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>This will delete {filteredCount} backtest{filteredCount !== 1 ? 's' : ''} that match the criteria</span>
</div> </div>
</div> </div>
<div className="modal-action"> <div className="modal-action">
<button type="submit" className="btn"> <button type="submit" className="btn btn-error" disabled={filteredCount === 0}>
Run Delete {filteredCount} Backtest{filteredCount !== 1 ? 's' : ''}
</button> </button>
</div> </div>
</Modal> </Modal>