diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 081e3164..830cebcb 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -243,7 +243,8 @@ public class BacktestController : BaseController [FromQuery] string? tickers = null, [FromQuery] string? indicators = null, [FromQuery] double? durationMinDays = null, - [FromQuery] double? durationMaxDays = null) + [FromQuery] double? durationMaxDays = null, + [FromQuery] string? name = null) { var user = await GetUser(); @@ -302,6 +303,7 @@ public class BacktestController : BaseController : indicators.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var filter = new BacktestsFilter { + NameContains = string.IsNullOrWhiteSpace(name) ? null : name.Trim(), ScoreMin = scoreMin, ScoreMax = scoreMax, WinrateMin = winrateMin, diff --git a/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs b/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs index 93712a53..77287152 100644 --- a/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs +++ b/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs @@ -2,6 +2,7 @@ namespace Managing.Application.Abstractions.Shared; public class BacktestsFilter { + public string? NameContains { get; set; } public double? ScoreMin { get; set; } public double? ScoreMax { get; set; } public int? WinrateMin { get; set; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs index 26c23987..9d33a06a 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs @@ -395,6 +395,11 @@ public class PostgreSqlBacktestRepository : IBacktestRepository if (filter != null) { + if (!string.IsNullOrWhiteSpace(filter.NameContains)) + { + var nameLike = $"%{filter.NameContains.Trim()}%"; + baseQuery = baseQuery.Where(b => EF.Functions.ILike(b.Name, nameLike)); + } if (filter.ScoreMin.HasValue) baseQuery = baseQuery.Where(b => b.Score >= filter.ScoreMin.Value); if (filter.ScoreMax.HasValue) diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index 217936d1..8f88a691 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -137,6 +137,7 @@ interface BacktestTableProps { currentSort?: { sortBy: BacktestSortableColumn; sortOrder: 'asc' | 'desc' } onBacktestDeleted?: () => void // Callback when a backtest is deleted onFiltersChange?: (filters: { + nameContains?: string | null scoreMin?: number | null scoreMax?: number | null winrateMin?: number | null @@ -148,6 +149,7 @@ interface BacktestTableProps { durationMaxDays?: number | null }) => void filters?: { + nameContains?: string | null scoreMin?: number | null scoreMax?: number | null winrateMin?: number | null @@ -185,6 +187,7 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh const [scoreMax, setScoreMax] = useState(100) const [winMin, setWinMin] = useState(0) const [winMax, setWinMax] = useState(100) + const [nameContains, setNameContains] = useState('') const [maxDrawdownMax, setMaxDrawdownMax] = useState('') const [tickersInput, setTickersInput] = useState('') const [selectedIndicators, setSelectedIndicators] = useState([]) @@ -194,6 +197,7 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh const applyFilters = () => { if (!onFiltersChange) return onFiltersChange({ + nameContains: nameContains.trim() || null, scoreMin, scoreMax, winrateMin: winMin, @@ -223,6 +227,7 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh // Sync incoming filters prop to local sidebar state useEffect(() => { if (!filters) return + setNameContains(filters.nameContains ?? '') if (typeof filters.scoreMin === 'number') setScoreMin(filters.scoreMin) if (typeof filters.scoreMax === 'number') setScoreMax(filters.scoreMax) if (typeof filters.winrateMin === 'number') setWinMin(filters.winrateMin) @@ -600,6 +605,18 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh + {/* Name contains */} +
+
Name contains
+ setNameContains(e.target.value)} + /> +
+ {/* Tickers */}
Tickers
diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 3c176abb..5a75cd2c 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -665,7 +665,7 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - 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 { + 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, name: string | null | undefined): Promise { let url_ = this.baseUrl + "/Backtest/Paginated?"; if (page === null) throw new Error("The parameter 'page' cannot be null."); @@ -699,6 +699,8 @@ export class BacktestClient extends AuthorizedApiBase { url_ += "durationMinDays=" + encodeURIComponent("" + durationMinDays) + "&"; if (durationMaxDays !== undefined && durationMaxDays !== null) url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&"; + if (name !== undefined && name !== null) + url_ += "name=" + encodeURIComponent("" + name) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx index e29f4b70..845be7e4 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx @@ -28,6 +28,7 @@ const BacktestScanner: React.FC = () => { // Filters state coming from BacktestTable sidebar const [filters, setFilters] = useState<{ + nameContains?: string | null scoreMin?: number | null scoreMax?: number | null winrateMin?: number | null @@ -57,6 +58,7 @@ const BacktestScanner: React.FC = () => { PAGE_SIZE, currentSort.sortBy, currentSort.sortOrder, + // filters filters.scoreMin ?? null, filters.scoreMax ?? null, filters.winrateMin ?? null, @@ -66,6 +68,7 @@ const BacktestScanner: React.FC = () => { (filters.indicators && filters.indicators.length ? filters.indicators.join(',') : null), filters.durationMinDays ?? null, filters.durationMaxDays ?? null, + filters.nameContains ?? null, ) return { backtests: (response.backtests as LightBacktestResponse[]) || [],