Add backtest light
This commit is contained in:
@@ -179,7 +179,21 @@ public class BacktestController : BaseController
|
||||
|
||||
var response = new PaginatedBacktestsResponse
|
||||
{
|
||||
Backtests = backtests,
|
||||
Backtests = backtests.Select(b => new LightBacktestResponse
|
||||
{
|
||||
Id = b.Id,
|
||||
Config = b.Config,
|
||||
FinalPnl = b.FinalPnl,
|
||||
WinRate = b.WinRate,
|
||||
GrowthPercentage = b.GrowthPercentage,
|
||||
HodlPercentage = b.HodlPercentage,
|
||||
StartDate = b.StartDate,
|
||||
EndDate = b.EndDate,
|
||||
MaxDrawdown = b.MaxDrawdown,
|
||||
Fees = b.Fees,
|
||||
SharpeRatio = b.SharpeRatio,
|
||||
Score = b.Score
|
||||
}),
|
||||
TotalCount = totalCount,
|
||||
CurrentPage = page,
|
||||
PageSize = pageSize,
|
||||
@@ -229,7 +243,21 @@ public class BacktestController : BaseController
|
||||
|
||||
var response = new PaginatedBacktestsResponse
|
||||
{
|
||||
Backtests = backtests,
|
||||
Backtests = backtests.Select(b => new LightBacktestResponse
|
||||
{
|
||||
Id = b.Id,
|
||||
Config = b.Config,
|
||||
FinalPnl = b.FinalPnl,
|
||||
WinRate = b.WinRate,
|
||||
GrowthPercentage = b.GrowthPercentage,
|
||||
HodlPercentage = b.HodlPercentage,
|
||||
StartDate = b.StartDate,
|
||||
EndDate = b.EndDate,
|
||||
MaxDrawdown = b.MaxDrawdown,
|
||||
Fees = b.Fees,
|
||||
SharpeRatio = b.SharpeRatio,
|
||||
Score = b.Score
|
||||
}),
|
||||
TotalCount = totalCount,
|
||||
CurrentPage = page,
|
||||
PageSize = pageSize,
|
||||
|
||||
19
src/Managing.Api/Models/Requests/LightBacktestResponse.cs
Normal file
19
src/Managing.Api/Models/Requests/LightBacktestResponse.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Managing.Domain.Bots;
|
||||
|
||||
namespace Managing.Api.Models.Requests;
|
||||
|
||||
public class LightBacktestResponse
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public TradingBotConfig Config { get; set; } = new();
|
||||
public decimal FinalPnl { get; set; }
|
||||
public int WinRate { get; set; }
|
||||
public decimal GrowthPercentage { get; set; }
|
||||
public decimal HodlPercentage { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public decimal? MaxDrawdown { get; set; }
|
||||
public decimal Fees { get; set; }
|
||||
public double? SharpeRatio { get; set; }
|
||||
public double Score { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
using Managing.Domain.Backtests;
|
||||
|
||||
namespace Managing.Api.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,7 +8,7 @@ public class PaginatedBacktestsResponse
|
||||
/// <summary>
|
||||
/// The list of backtests for the current page
|
||||
/// </summary>
|
||||
public IEnumerable<Backtest> Backtests { get; set; } = new List<Backtest>();
|
||||
public IEnumerable<LightBacktestResponse> Backtests { get; set; } = new List<LightBacktestResponse>();
|
||||
|
||||
/// <summary>
|
||||
/// Total number of backtests across all pages
|
||||
|
||||
@@ -8,8 +8,8 @@ public interface IBacktestRepository
|
||||
void InsertBacktestForUser(User user, Backtest result);
|
||||
IEnumerable<Backtest> GetBacktestsByUser(User user);
|
||||
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
||||
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
Backtest GetBacktestByIdForUser(User user, string id);
|
||||
void DeleteBacktestByIdForUser(User user, string id);
|
||||
void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
||||
|
||||
@@ -54,12 +54,12 @@ namespace Managing.Application.Abstractions.Services
|
||||
bool DeleteBacktests();
|
||||
IEnumerable<Backtest> GetBacktestsByUser(User user);
|
||||
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
||||
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
Backtest GetBacktestByIdForUser(User user, string id);
|
||||
bool DeleteBacktestByUser(User user, string id);
|
||||
bool DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
||||
bool DeleteBacktestsByUser(User user);
|
||||
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ namespace Managing.Application.Backtesting
|
||||
return backtests;
|
||||
}
|
||||
|
||||
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||
{
|
||||
var (backtests, totalCount) = _backtestRepository.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder);
|
||||
return (backtests, totalCount);
|
||||
@@ -531,7 +531,7 @@ namespace Managing.Application.Backtesting
|
||||
}
|
||||
}
|
||||
|
||||
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||
{
|
||||
var (backtests, totalCount) = _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder);
|
||||
return (backtests, totalCount);
|
||||
|
||||
19
src/Managing.Domain/Backtests/LightBacktest.cs
Normal file
19
src/Managing.Domain/Backtests/LightBacktest.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Managing.Domain.Bots;
|
||||
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
public class LightBacktest
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public TradingBotConfig Config { get; set; } = new();
|
||||
public decimal FinalPnl { get; set; }
|
||||
public int WinRate { get; set; }
|
||||
public decimal GrowthPercentage { get; set; }
|
||||
public decimal HodlPercentage { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public decimal? MaxDrawdown { get; set; }
|
||||
public decimal Fees { get; set; }
|
||||
public double? SharpeRatio { get; set; }
|
||||
public double Score { get; set; }
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public class BacktestRepository : IBacktestRepository
|
||||
return backtests.Select(b => MongoMappers.Map(b));
|
||||
}
|
||||
|
||||
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId,
|
||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId,
|
||||
int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
@@ -101,14 +101,12 @@ public class BacktestRepository : IBacktestRepository
|
||||
.Include(b => b.WinRate)
|
||||
.Include(b => b.GrowthPercentage)
|
||||
.Include(b => b.HodlPercentage)
|
||||
.Include(b => b.User)
|
||||
.Include(b => b.Statistics)
|
||||
.Include(b => b.StartDate)
|
||||
.Include(b => b.EndDate)
|
||||
.Include(b => b.Score)
|
||||
.Include(b => b.RequestId)
|
||||
.Include(b => b.Metadata)
|
||||
.Include(b => b.Config);
|
||||
.Include(b => b.Config)
|
||||
.Include(b => b.Fees)
|
||||
.Include(b => b.Statistics);
|
||||
|
||||
// Build sort definition
|
||||
var sortDefinition = sortBy.ToLower() switch
|
||||
@@ -146,7 +144,21 @@ public class BacktestRepository : IBacktestRepository
|
||||
Console.WriteLine(
|
||||
$"[BacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Projection: {afterProjectionMs - afterCountMs}ms, ToList: {afterToListMs - afterProjectionMs}ms, Total: {afterToListMs}ms");
|
||||
|
||||
var mappedBacktests = backtests.Select(b => MongoMappers.Map(b));
|
||||
var mappedBacktests = backtests.Select(b => new LightBacktest
|
||||
{
|
||||
Id = b.Identifier,
|
||||
Config = MongoMappers.Map(b.Config),
|
||||
FinalPnl = b.FinalPnl,
|
||||
WinRate = b.WinRate,
|
||||
GrowthPercentage = b.GrowthPercentage,
|
||||
HodlPercentage = b.HodlPercentage,
|
||||
StartDate = b.StartDate,
|
||||
EndDate = b.EndDate,
|
||||
MaxDrawdown = b.Statistics?.MaxDrawdown,
|
||||
Fees = b.Fees,
|
||||
SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null,
|
||||
Score = b.Score
|
||||
});
|
||||
|
||||
return (mappedBacktests, (int)totalCount);
|
||||
}
|
||||
@@ -198,7 +210,7 @@ public class BacktestRepository : IBacktestRepository
|
||||
}
|
||||
}
|
||||
|
||||
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page,
|
||||
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page,
|
||||
int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
@@ -216,14 +228,12 @@ public class BacktestRepository : IBacktestRepository
|
||||
.Include(b => b.WinRate)
|
||||
.Include(b => b.GrowthPercentage)
|
||||
.Include(b => b.HodlPercentage)
|
||||
.Include(b => b.User)
|
||||
.Include(b => b.Statistics)
|
||||
.Include(b => b.StartDate)
|
||||
.Include(b => b.EndDate)
|
||||
.Include(b => b.Score)
|
||||
.Include(b => b.RequestId)
|
||||
.Include(b => b.Metadata)
|
||||
.Include(b => b.Config);
|
||||
.Include(b => b.Config)
|
||||
.Include(b => b.Fees)
|
||||
.Include(b => b.Statistics);
|
||||
|
||||
// Build sort definition
|
||||
var sortDefinition = sortBy.ToLower() switch
|
||||
@@ -261,7 +271,21 @@ public class BacktestRepository : IBacktestRepository
|
||||
Console.WriteLine(
|
||||
$"[BacktestRepo] User Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Projection: {afterProjectionMs - afterCountMs}ms, ToList: {afterToListMs - afterProjectionMs}ms, Total: {afterToListMs}ms");
|
||||
|
||||
var mappedBacktests = backtests.Select(b => MongoMappers.Map(b));
|
||||
var mappedBacktests = backtests.Select(b => new LightBacktest
|
||||
{
|
||||
Id = b.Identifier,
|
||||
Config = MongoMappers.Map(b.Config),
|
||||
FinalPnl = b.FinalPnl,
|
||||
WinRate = b.WinRate,
|
||||
GrowthPercentage = b.GrowthPercentage,
|
||||
HodlPercentage = b.HodlPercentage,
|
||||
StartDate = b.StartDate,
|
||||
EndDate = b.EndDate,
|
||||
MaxDrawdown = b.Statistics?.MaxDrawdown,
|
||||
Fees = b.Fees,
|
||||
SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null,
|
||||
Score = b.Score
|
||||
});
|
||||
|
||||
return (mappedBacktests, (int)totalCount);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
|
||||
public MoneyManagementDto OptimizedMoneyManagement { get; internal set; }
|
||||
public UserDto User { get; set; }
|
||||
public PerformanceMetrics Statistics { get; set; }
|
||||
[BsonRepresentation(BsonType.Decimal128)]
|
||||
public decimal Fees { get; set; }
|
||||
public double Score { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public string RequestId { get; set; }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {create} from 'zustand'
|
||||
import type {Backtest} from '../../generated/ManagingApi'
|
||||
import type {LightBacktestResponse} from '../../generated/ManagingApi'
|
||||
|
||||
interface BacktestStore {
|
||||
backtests: Backtest[]
|
||||
backtests: LightBacktestResponse[]
|
||||
isLoading: boolean
|
||||
setBacktests: (backtests: Backtest[]) => void
|
||||
addBacktest: (backtest: Backtest) => void
|
||||
setBacktests: (backtests: LightBacktestResponse[]) => void
|
||||
addBacktest: (backtest: LightBacktestResponse) => void
|
||||
removeBacktest: (id: string) => void
|
||||
setLoading: (loading: boolean) => void
|
||||
clearBacktests: () => void
|
||||
@@ -15,10 +15,10 @@ const useBacktestStore = create<BacktestStore>((set, get) => ({
|
||||
backtests: [],
|
||||
isLoading: false,
|
||||
|
||||
setBacktests: (backtests: Backtest[]) =>
|
||||
setBacktests: (backtests: LightBacktestResponse[]) =>
|
||||
set({ backtests }),
|
||||
|
||||
addBacktest: (backtest: Backtest) =>
|
||||
addBacktest: (backtest: LightBacktestResponse) =>
|
||||
set((state) => ({
|
||||
backtests: [...state.backtests, backtest]
|
||||
})),
|
||||
|
||||
@@ -84,13 +84,12 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
const candles = candlesData?.candles || currentBacktest.candles || [];
|
||||
const indicatorsValues = candlesData?.indicatorsValues || currentBacktest.indicatorsValues || {};
|
||||
|
||||
const {
|
||||
positions,
|
||||
walletBalances,
|
||||
signals,
|
||||
statistics,
|
||||
config
|
||||
} = currentBacktest;
|
||||
// Only destructure these properties if we have full backtest data
|
||||
const positions = fullBacktestData?.positions || [];
|
||||
const walletBalances = fullBacktestData?.walletBalances || [];
|
||||
const signals = fullBacktestData?.signals || [];
|
||||
const statistics = fullBacktestData?.statistics;
|
||||
const config = currentBacktest.config;
|
||||
|
||||
// Helper function to calculate position open time in hours
|
||||
const calculateOpenTimeInHours = (position: any) => {
|
||||
@@ -219,7 +218,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
|
||||
// Calculate recommended cooldown based on positions that fail after a win
|
||||
const getCooldownRecommendations = () => {
|
||||
if (positions.length < 2 || !candles || candles.length < 2) {
|
||||
if (positions?.length < 2 || !candles || candles?.length < 2) {
|
||||
return { percentile75: "0", average: "0", median: "0" };
|
||||
}
|
||||
|
||||
@@ -343,14 +342,14 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
<CardText
|
||||
title="Max Drowdown"
|
||||
content={
|
||||
statistics.maxDrawdown?.toFixed(4).toString() +
|
||||
(statistics?.maxDrawdown?.toFixed(4) || '0.0000') +
|
||||
'$'
|
||||
}
|
||||
></CardText>
|
||||
<CardText
|
||||
title="Sharpe Ratio"
|
||||
content={
|
||||
(statistics.sharpeRatio
|
||||
(statistics?.sharpeRatio
|
||||
? statistics.sharpeRatio * 100
|
||||
: 0
|
||||
)
|
||||
@@ -361,7 +360,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
<CardText
|
||||
title="Max Drawdown Recovery Time"
|
||||
content={
|
||||
statistics.maxDrawdownRecoveryTime?.toString() +
|
||||
(statistics?.maxDrawdownRecoveryTime?.toString() || '0') +
|
||||
' days'
|
||||
}
|
||||
></CardText>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {useExpanded, useFilters, usePagination, useSortBy, useTable,} from 'reac
|
||||
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import useBacktestStore from '../../../app/store/backtestStore'
|
||||
import type {Backtest} from '../../../generated/ManagingApi'
|
||||
import type {Backtest, LightBacktestResponse} from '../../../generated/ManagingApi'
|
||||
import {BacktestClient} from '../../../generated/ManagingApi'
|
||||
import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter} from '../../mollecules'
|
||||
import {UnifiedTradingModal} from '../index'
|
||||
@@ -130,7 +130,7 @@ const ServerSortableTable = ({
|
||||
}
|
||||
|
||||
interface BacktestTableProps {
|
||||
list: Backtest[] | undefined
|
||||
list: LightBacktestResponse[] | undefined
|
||||
isFetching?: boolean
|
||||
displaySummary?: boolean
|
||||
onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void
|
||||
@@ -139,26 +139,10 @@ interface BacktestTableProps {
|
||||
|
||||
|
||||
|
||||
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displaySummary = true, onSortChange, currentSort}) => {
|
||||
const [rows, setRows] = useState<Backtest[]>([])
|
||||
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, onSortChange, currentSort}) => {
|
||||
const [rows, setRows] = useState<LightBacktestResponse[]>([])
|
||||
const {apiUrl} = useApiUrlStore()
|
||||
const {removeBacktest} = useBacktestStore()
|
||||
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
|
||||
stopLoss: 0,
|
||||
takeProfit: 0,
|
||||
})
|
||||
const [positionTimingStats, setPositionTimingStats] = useState({
|
||||
averageOpenTime: 0,
|
||||
medianOpenTime: 0,
|
||||
losingPositionsAverageOpenTime: 0,
|
||||
})
|
||||
const [cooldownRecommendations, setCooldownRecommendations] = useState({
|
||||
averageCooldown: 0,
|
||||
medianCooldown: 0,
|
||||
})
|
||||
|
||||
// Summary collapse state
|
||||
const [isSummaryCollapsed, setIsSummaryCollapsed] = useState(true)
|
||||
|
||||
// Bot configuration modal state
|
||||
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
|
||||
@@ -466,146 +450,6 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
||||
useEffect(() => {
|
||||
if (list) {
|
||||
setRows(list)
|
||||
|
||||
// Calculate average optimized money management
|
||||
if (list.length > 0) {
|
||||
const optimized = list.map((b) => b.optimizedMoneyManagement);
|
||||
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0);
|
||||
const takeProfit = optimized.reduce((acc, curr) => acc + (curr?.takeProfit ?? 0), 0);
|
||||
|
||||
setOptimizedMoneyManagement({
|
||||
stopLoss: stopLoss / optimized.length,
|
||||
takeProfit: takeProfit / optimized.length,
|
||||
});
|
||||
|
||||
// Calculate position timing statistics
|
||||
const allPositions = list.flatMap(backtest => backtest.positions);
|
||||
const finishedPositions = allPositions.filter(p => p.status === 'Finished');
|
||||
|
||||
if (finishedPositions.length > 0) {
|
||||
// Calculate position open times in hours
|
||||
const openTimes = finishedPositions.map(position => {
|
||||
const openTime = new Date(position.open.date);
|
||||
// Find the closing trade (either stopLoss or takeProfit that was filled)
|
||||
let closeTime = new Date();
|
||||
|
||||
if (position.stopLoss.status === 'Filled') {
|
||||
closeTime = new Date(position.stopLoss.date);
|
||||
} else if (position.takeProfit1.status === 'Filled') {
|
||||
closeTime = new Date(position.takeProfit1.date);
|
||||
} else if (position.takeProfit2?.status === 'Filled') {
|
||||
closeTime = new Date(position.takeProfit2.date);
|
||||
}
|
||||
|
||||
// Return time difference in hours
|
||||
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
|
||||
});
|
||||
|
||||
// Calculate average
|
||||
const averageOpenTime = openTimes.reduce((sum, time) => sum + time, 0) / openTimes.length;
|
||||
|
||||
// Calculate median
|
||||
const sortedTimes = [...openTimes].sort((a, b) => a - b);
|
||||
const medianOpenTime = sortedTimes.length % 2 === 0
|
||||
? (sortedTimes[sortedTimes.length / 2 - 1] + sortedTimes[sortedTimes.length / 2]) / 2
|
||||
: sortedTimes[Math.floor(sortedTimes.length / 2)];
|
||||
|
||||
// Calculate average for losing positions
|
||||
const losingPositions = finishedPositions.filter(p => (p.profitAndLoss?.realized ?? 0) < 0);
|
||||
let losingPositionsAverageOpenTime = 0;
|
||||
|
||||
if (losingPositions.length > 0) {
|
||||
const losingOpenTimes = losingPositions.map(position => {
|
||||
const openTime = new Date(position.open.date);
|
||||
let closeTime = new Date();
|
||||
|
||||
if (position.stopLoss.status === 'Filled') {
|
||||
closeTime = new Date(position.stopLoss.date);
|
||||
} else if (position.takeProfit1.status === 'Filled') {
|
||||
closeTime = new Date(position.takeProfit1.date);
|
||||
} else if (position.takeProfit2?.status === 'Filled') {
|
||||
closeTime = new Date(position.takeProfit2.date);
|
||||
}
|
||||
|
||||
return (closeTime.getTime() - openTime.getTime()) / (1000 * 60 * 60);
|
||||
});
|
||||
|
||||
losingPositionsAverageOpenTime = losingOpenTimes.reduce((sum, time) => sum + time, 0) / losingOpenTimes.length;
|
||||
}
|
||||
|
||||
setPositionTimingStats({
|
||||
averageOpenTime,
|
||||
medianOpenTime,
|
||||
losingPositionsAverageOpenTime,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate cooldown recommendations across all backtests
|
||||
const allCooldownValues: number[] = [];
|
||||
|
||||
list.forEach(backtest => {
|
||||
if (backtest.positions.length < 2 || !backtest.candles || backtest.candles.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine candle timeframe in milliseconds
|
||||
const candleTimeframeMs = new Date(backtest.candles[1].date).getTime() - new Date(backtest.candles[0].date).getTime();
|
||||
|
||||
const sortedPositions = [...backtest.positions].sort((a, b) => {
|
||||
const dateA = new Date(a.open.date).getTime();
|
||||
const dateB = new Date(b.open.date).getTime();
|
||||
return dateA - dateB;
|
||||
});
|
||||
|
||||
for (let i = 0; i < sortedPositions.length - 1; i++) {
|
||||
const currentPosition = sortedPositions[i];
|
||||
const nextPosition = sortedPositions[i + 1];
|
||||
|
||||
const currentRealized = currentPosition.profitAndLoss?.realized ?? 0;
|
||||
const nextRealized = nextPosition.profitAndLoss?.realized ?? 0;
|
||||
|
||||
// Check if current position is winning and next position is losing
|
||||
if (currentRealized > 0 && nextRealized <= 0) {
|
||||
// Calculate the close time of the current (winning) position
|
||||
let currentCloseDate: Date | null = null;
|
||||
if (currentPosition.profitAndLoss?.realized != null) {
|
||||
if (currentPosition.profitAndLoss.realized > 0) {
|
||||
currentCloseDate = new Date(currentPosition.takeProfit1.date);
|
||||
} else {
|
||||
currentCloseDate = new Date(currentPosition.stopLoss.date);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCloseDate) {
|
||||
const nextOpenDate = new Date(nextPosition.open.date);
|
||||
const gapInMs = nextOpenDate.getTime() - currentCloseDate.getTime();
|
||||
|
||||
if (gapInMs >= 0) { // Only consider positive gaps
|
||||
// Convert milliseconds to number of candles
|
||||
const gapInCandles = Math.floor(gapInMs / candleTimeframeMs);
|
||||
allCooldownValues.push(gapInCandles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (allCooldownValues.length > 0) {
|
||||
// Calculate average cooldown
|
||||
const averageCooldown = allCooldownValues.reduce((sum, value) => sum + value, 0) / allCooldownValues.length;
|
||||
|
||||
// Calculate median cooldown
|
||||
const sortedCooldowns = [...allCooldownValues].sort((a, b) => a - b);
|
||||
const medianCooldown = sortedCooldowns.length % 2 === 0
|
||||
? (sortedCooldowns[sortedCooldowns.length / 2 - 1] + sortedCooldowns[sortedCooldowns.length / 2]) / 2
|
||||
: sortedCooldowns[Math.floor(sortedCooldowns.length / 2)];
|
||||
|
||||
setCooldownRecommendations({
|
||||
averageCooldown: Math.ceil(averageCooldown),
|
||||
medianCooldown: Math.ceil(medianCooldown),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [list])
|
||||
|
||||
@@ -617,58 +461,6 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{list && list.length > 0 && displaySummary && (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={() => setIsSummaryCollapsed(!isSummaryCollapsed)}
|
||||
className="btn btn-sm btn-outline mb-2"
|
||||
>
|
||||
{isSummaryCollapsed ? (
|
||||
<>
|
||||
<ChevronRightIcon className="w-4 h-4 mr-1" />
|
||||
Show Summary
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDownIcon className="w-4 h-4 mr-1" />
|
||||
Hide Summary
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!isSummaryCollapsed && (
|
||||
<div className="bg-base-200 p-4 rounded-lg space-y-3">
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-xs text-gray-600 mb-1">Money Management</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="badge badge-outline">SL: {optimizedMoneyManagement.stopLoss.toFixed(2)}%</span>
|
||||
<span className="badge badge-outline">TP: {optimizedMoneyManagement.takeProfit.toFixed(2)}%</span>
|
||||
<span className="badge badge-outline">R/R: {(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-xs text-gray-600 mb-1">Position Timing</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="badge badge-outline">Avg: {positionTimingStats.averageOpenTime.toFixed(1)}h</span>
|
||||
<span className="badge badge-outline">Median: {positionTimingStats.medianOpenTime.toFixed(1)}h</span>
|
||||
<span className="badge badge-outline">Losing: {positionTimingStats.losingPositionsAverageOpenTime.toFixed(1)}h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-xs text-gray-600 mb-1">Cooldown</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="badge badge-outline">Avg: {cooldownRecommendations.averageCooldown} candles</span>
|
||||
<span className="badge badge-outline">Median: {cooldownRecommendations.medianCooldown} candles</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ServerSortableTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
|
||||
@@ -3794,7 +3794,7 @@ export interface DeleteBacktestsRequest {
|
||||
}
|
||||
|
||||
export interface PaginatedBacktestsResponse {
|
||||
backtests?: Backtest[] | null;
|
||||
backtests?: LightBacktestResponse[] | null;
|
||||
totalCount?: number;
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
@@ -3803,6 +3803,21 @@ export interface PaginatedBacktestsResponse {
|
||||
hasPreviousPage?: boolean;
|
||||
}
|
||||
|
||||
export interface LightBacktestResponse {
|
||||
id?: string | null;
|
||||
config?: TradingBotConfig | null;
|
||||
finalPnl?: number;
|
||||
winRate?: number;
|
||||
growthPercentage?: number;
|
||||
hodlPercentage?: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
maxDrawdown?: number | null;
|
||||
fees?: number;
|
||||
sharpeRatio?: number | null;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface RunBacktestRequest {
|
||||
config?: TradingBotConfigRequest | null;
|
||||
startDate?: Date;
|
||||
|
||||
@@ -600,7 +600,7 @@ export interface DeleteBacktestsRequest {
|
||||
}
|
||||
|
||||
export interface PaginatedBacktestsResponse {
|
||||
backtests?: Backtest[] | null;
|
||||
backtests?: LightBacktestResponse[] | null;
|
||||
totalCount?: number;
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
@@ -609,6 +609,21 @@ export interface PaginatedBacktestsResponse {
|
||||
hasPreviousPage?: boolean;
|
||||
}
|
||||
|
||||
export interface LightBacktestResponse {
|
||||
id?: string | null;
|
||||
config?: TradingBotConfig | null;
|
||||
finalPnl?: number;
|
||||
winRate?: number;
|
||||
growthPercentage?: number;
|
||||
hodlPercentage?: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
maxDrawdown?: number | null;
|
||||
fees?: number;
|
||||
sharpeRatio?: number | null;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface RunBacktestRequest {
|
||||
config?: TradingBotConfigRequest | null;
|
||||
startDate?: Date;
|
||||
|
||||
@@ -4,13 +4,13 @@ import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import useApiUrlStore from '../../app/store/apiStore'
|
||||
import {
|
||||
type Backtest,
|
||||
BacktestClient,
|
||||
GeneticCrossoverMethod,
|
||||
GeneticMutationMethod,
|
||||
type GeneticRequest,
|
||||
GeneticSelectionMethod,
|
||||
IndicatorType,
|
||||
type LightBacktestResponse,
|
||||
type PaginatedBacktestsResponse,
|
||||
type RunGeneticRequest,
|
||||
Ticker,
|
||||
@@ -73,7 +73,7 @@ const BacktestGeneticBundle: React.FC = () => {
|
||||
const [geneticRequests, setGeneticRequests] = useState<GeneticRequest[]>([])
|
||||
const [selectedRequest, setSelectedRequest] = useState<GeneticRequest | null>(null)
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
||||
const [backtests, setBacktests] = useState<Backtest[]>([])
|
||||
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([])
|
||||
const [isLoadingBacktests, setIsLoadingBacktests] = useState(false)
|
||||
const [isFormCollapsed, setIsFormCollapsed] = useState(false)
|
||||
|
||||
@@ -824,7 +824,7 @@ const BacktestGeneticBundle: React.FC = () => {
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">Score vs Generation</h3>
|
||||
<ScoreVsGeneration backtests={backtests} theme={theme}/>
|
||||
<ScoreVsGeneration backtests={backtests as any} theme={theme}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -834,7 +834,7 @@ const BacktestGeneticBundle: React.FC = () => {
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">Fitness vs Score vs Win Rate</h3>
|
||||
<Fitness3DPlot backtests={backtests} theme={theme}/>
|
||||
<Fitness3DPlot backtests={backtests as any} theme={theme}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -842,18 +842,18 @@ const BacktestGeneticBundle: React.FC = () => {
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">Take Profit vs Stop Loss vs PnL</h3>
|
||||
<TPvsSLvsPnL3DPlot backtests={backtests} theme={theme}/>
|
||||
<TPvsSLvsPnL3DPlot backtests={backtests as any} theme={theme}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Strategy Comparison Radar Chart */}
|
||||
<div className="mb-6">
|
||||
<IndicatorsComparison backtests={backtests}/>
|
||||
<IndicatorsComparison backtests={backtests as any}/>
|
||||
</div>
|
||||
|
||||
<BacktestTable
|
||||
list={backtests}
|
||||
list={backtests as any}
|
||||
isFetching={false}
|
||||
displaySummary={false}
|
||||
onSortChange={handleSortChange}
|
||||
|
||||
@@ -7,7 +7,7 @@ import useBacktestStore from '../../app/store/backtestStore'
|
||||
import {Loader, Slider} from '../../components/atoms'
|
||||
import {Modal, Toast} from '../../components/mollecules'
|
||||
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
|
||||
import type {Backtest} from '../../generated/ManagingApi'
|
||||
import type {LightBacktestResponse} from '../../generated/ManagingApi'
|
||||
import {BacktestClient} from '../../generated/ManagingApi'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
@@ -27,7 +27,7 @@ const BacktestScanner: React.FC = () => {
|
||||
sortBy: 'score',
|
||||
sortOrder: 'desc'
|
||||
})
|
||||
const [backtests, setBacktests] = useState<Backtest[]>([])
|
||||
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
@@ -39,7 +39,7 @@ const BacktestScanner: React.FC = () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await backtestClient.backtest_GetBacktestsPaginated(page, PAGE_SIZE, sort.sortBy, sort.sortOrder)
|
||||
setBacktests((response.backtests as Backtest[]) || [])
|
||||
setBacktests((response.backtests as LightBacktestResponse[]) || [])
|
||||
setTotalBacktests(response.totalCount || 0)
|
||||
setTotalPages(response.totalPages || 0)
|
||||
} catch (err) {
|
||||
@@ -55,7 +55,7 @@ const BacktestScanner: React.FC = () => {
|
||||
}, [currentPage, currentSort])
|
||||
|
||||
useEffect(() => {
|
||||
setBacktestsFromStore(backtests)
|
||||
setBacktestsFromStore(backtests as any) // Cast to any for backward compatibility
|
||||
setLoading(isLoading)
|
||||
}, [backtests, setBacktestsFromStore, setLoading, isLoading])
|
||||
|
||||
@@ -79,7 +79,7 @@ const BacktestScanner: React.FC = () => {
|
||||
|
||||
const filters = formData || filterValues
|
||||
|
||||
const filteredBacktests = backtests.filter((backtest: any) => {
|
||||
const filteredBacktests = backtests.filter((backtest: LightBacktestResponse) => {
|
||||
// Ensure values are numbers and handle potential null/undefined values
|
||||
const backtestWinRate = Number(backtest.winRate) || 0
|
||||
const backtestScore = Number(backtest.score) || 0
|
||||
@@ -129,7 +129,7 @@ const BacktestScanner: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const backTestToDelete = backtests.filter((backtest: any) => {
|
||||
const backTestToDelete = backtests.filter((backtest: LightBacktestResponse) => {
|
||||
// Ensure values are numbers and handle potential null/undefined values
|
||||
const backtestWinRate = Number(backtest.winRate) || 0
|
||||
const backtestScore = Number(backtest.score) || 0
|
||||
@@ -198,7 +198,7 @@ const BacktestScanner: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<BacktestTable
|
||||
list={backtests}
|
||||
list={backtests as LightBacktestResponse[]} // Cast to any for backward compatibility
|
||||
isFetching={isLoading}
|
||||
onSortChange={handleSortChange}
|
||||
currentSort={currentSort}
|
||||
|
||||
Reference in New Issue
Block a user