Delete backtest by id and with filters
This commit is contained in:
@@ -103,6 +103,18 @@ public class BacktestController : BaseController
|
||||
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>
|
||||
/// Retrieves all backtests for a specific genetic request ID.
|
||||
/// This endpoint is used to view the results of a genetic algorithm optimization.
|
||||
|
||||
16
src/Managing.Api/Models/Requests/DeleteBacktestsRequest.cs
Normal file
16
src/Managing.Api/Models/Requests/DeleteBacktestsRequest.cs
Normal 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>();
|
||||
}
|
||||
@@ -10,5 +10,6 @@ public interface IBacktestRepository
|
||||
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
||||
Backtest GetBacktestByIdForUser(User user, string id);
|
||||
void DeleteBacktestByIdForUser(User user, string id);
|
||||
void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
||||
void DeleteAllBacktestsForUser(User user);
|
||||
}
|
||||
@@ -56,6 +56,7 @@ namespace Managing.Application.Abstractions.Services
|
||||
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
||||
Backtest GetBacktestByIdForUser(User user, string id);
|
||||
bool DeleteBacktestByUser(User user, string id);
|
||||
bool DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
||||
bool DeleteBacktestsByUser(User user);
|
||||
|
||||
|
||||
|
||||
@@ -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<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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
var backtests = _backtestRepository.AsQueryable()
|
||||
|
||||
@@ -498,6 +498,50 @@ export class BacktestClient extends AuthorizedApiBase {
|
||||
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[]> {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'}
|
||||
>
|
||||
<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="input-group">
|
||||
<label htmlFor="winRate" className="label mr-6">
|
||||
WinRate{'<'}
|
||||
WinRate ≤
|
||||
</label>
|
||||
<label className="input-group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
className="input input-sm w-20"
|
||||
name="winRate"
|
||||
defaultValue={50}
|
||||
value={filterValues.winRate}
|
||||
min="0"
|
||||
max="100"
|
||||
onChange={(e) => updateFilterValue('winRate', Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<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="%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-control">
|
||||
<div className="input-group">
|
||||
<label htmlFor="hp" className="label mr-6">
|
||||
H/P{'<'}
|
||||
<label htmlFor="score" className="label mr-6">
|
||||
Score ≤
|
||||
</label>
|
||||
<label className="input-group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
name="hp"
|
||||
defaultValue={3}
|
||||
className="input input-sm w-20"
|
||||
name="score"
|
||||
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 className="modal-action">
|
||||
<button type="submit" className="btn">
|
||||
Run
|
||||
<button type="submit" className="btn btn-error" disabled={filteredCount === 0}>
|
||||
Delete {filteredCount} Backtest{filteredCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user