import {CardPositionItem, TradeChart} from '..' import { Backtest, BacktestClient, CandlesWithIndicatorsResponse, DataClient, GetCandlesWithIndicatorsRequest, IndicatorType, LightSignal, Position, SignalType } from '../../../generated/ManagingApi' import {CardPosition, CardText} from '../../mollecules' import {useQuery} from '@tanstack/react-query' import useApiUrlStore from '../../../app/store/apiStore' interface IBacktestRowDetailsProps { backtest: Backtest; } const BacktestRowDetails: React.FC = ({ backtest }) => { const {apiUrl} = useApiUrlStore(); const dataClient = new DataClient({}, apiUrl); const backtestClient = new BacktestClient({}, apiUrl); // Use TanStack Query to load full backtest data const {data: fullBacktestData, isLoading: isLoadingFullBacktest, error: fullBacktestError} = useQuery({ queryKey: ['fullBacktest', backtest.id], queryFn: async (): Promise => { return await backtestClient.backtest_Backtest(backtest.id); }, staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) }); // Use TanStack Query to load candles with indicators const {data: candlesData, isLoading: isLoadingCandles, error} = useQuery({ queryKey: ['candlesWithIndicators', backtest.id, backtest.config?.scenario?.name], queryFn: async (): Promise => { // Only fetch if no candles are present if (backtest.candles && backtest.candles.length > 0) { return { candles: backtest.candles, indicatorsValues: {} // Default empty object since Backtest doesn't have indicatorsValues }; } const request: GetCandlesWithIndicatorsRequest = { ticker: backtest.config.ticker, startDate: backtest.startDate, endDate: backtest.endDate, timeframe: backtest.config.timeframe, scenario: backtest.config?.scenario ? { name: backtest.config.scenario.name || '', indicators: backtest.config.scenario.indicators?.map(indicator => ({ name: indicator.name || '', type: indicator.type || IndicatorType.RsiDivergence, signalType: indicator.signalType || SignalType.Signal, minimumHistory: indicator.minimumHistory || 0, period: indicator.period, fastPeriods: indicator.fastPeriods, slowPeriods: indicator.slowPeriods, signalPeriods: indicator.signalPeriods, multiplier: indicator.multiplier, smoothPeriods: indicator.smoothPeriods, stochPeriods: indicator.stochPeriods, cyclePeriods: indicator.cyclePeriods })) || [], loopbackPeriod: backtest.config.scenario.loopbackPeriod } : undefined }; return await dataClient.data_GetCandlesWithIndicators(request); }, enabled: !backtest.candles || backtest.candles.length === 0, // Only run query if no candles exist staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) }); // Use the full backtest data if available, otherwise fallback to the original backtest const currentBacktest = fullBacktestData || backtest; // Use the data from query or fallback to backtest data const candles = candlesData?.candles || currentBacktest.candles || []; const indicatorsValues = candlesData?.indicatorsValues || {}; // Convert positions and signals objects to arrays const positionsArray: Position[] = Object.values(currentBacktest.positions || {}); const signalsArray: LightSignal[] = Object.values(currentBacktest.signals || {}); const walletBalances = currentBacktest.walletBalances || []; const statistics = currentBacktest.statistics; const config = currentBacktest.config; // Helper function to calculate position open time in hours const calculateOpenTimeInHours = (position: Position): number => { const openDate = new Date(position.Open.date); let closeDate: Date | null = null; // Determine close date based on realized P&L (matching backend logic) if (position.ProfitAndLoss?.realized != null) { if (position.ProfitAndLoss.realized > 0) { // Profitable close = Take Profit closeDate = new Date(position.TakeProfit1.date); } else { // Loss or breakeven close = Stop Loss closeDate = new Date(position.StopLoss.date); } } if (closeDate) { const diffInMs = closeDate.getTime() - openDate.getTime(); return diffInMs / (1000 * 60 * 60); // Convert to hours } return 0; }; // Calculate average open time for winning positions const getAverageOpenTimeWinning = (): string => { const winningPositions = positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0; return realized > 0; }); if (winningPositions.length === 0) return "0.00"; const totalHours = winningPositions.reduce((sum: number, position: Position) => { return sum + calculateOpenTimeInHours(position); }, 0); const averageHours = totalHours / winningPositions.length; return averageHours.toFixed(2); }; // Calculate average open time for losing positions const getAverageOpenTimeLosing = (): string => { const losingPositions = positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0; return realized <= 0; }); if (losingPositions.length === 0) return "0.00"; const totalHours = losingPositions.reduce((sum: number, position: Position) => { return sum + calculateOpenTimeInHours(position); }, 0); const averageHours = totalHours / losingPositions.length; return averageHours.toFixed(2); }; // Calculate maximum open time for winning positions const getMaxOpenTimeWinning = (): string => { const winningPositions = positionsArray.filter((p: Position) => { const realized = p.ProfitAndLoss?.realized ?? 0; return realized > 0; }); if (winningPositions.length === 0) return "0.00"; const openTimes = winningPositions.map((position: Position) => calculateOpenTimeInHours(position)); const maxHours = Math.max(...openTimes); return maxHours.toFixed(2); }; // Calculate median opening time for all positions const getMedianOpenTime = (): string => { if (positionsArray.length === 0) return "0.00"; const openTimes = positionsArray.map((position: Position) => calculateOpenTimeInHours(position)); const sortedTimes = openTimes.sort((a: number, b: number) => a - b); const mid = Math.floor(sortedTimes.length / 2); const median = sortedTimes.length % 2 === 0 ? (sortedTimes[mid - 1] + sortedTimes[mid]) / 2 : sortedTimes[mid]; return median.toFixed(2); }; // Calculate total volume traded with leverage const getTotalVolumeTraded = (): number => { let totalVolume = 0; positionsArray.forEach((position: Position) => { // Calculate volume for open trade const openLeverage = position.Open.leverage || 1; const openVolume = position.Open.quantity * position.Open.price * openLeverage; totalVolume += openVolume; // Calculate volume for close trade (stopLoss or takeProfit based on realized P&L) if (position.ProfitAndLoss?.realized != null) { let closeTrade; if (position.ProfitAndLoss.realized > 0) { // Profitable close = Take Profit closeTrade = position.TakeProfit1; } else { // Loss or breakeven close = Stop Loss closeTrade = position.StopLoss; } if (closeTrade) { const closeLeverage = closeTrade.leverage || 1; const closeVolume = closeTrade.quantity * closeTrade.price * closeLeverage; totalVolume += closeVolume; } } }); return totalVolume; }; // Calculate estimated UI fee (0.02% of total volume) const getEstimatedUIFee = (): number => { const totalVolume = getTotalVolumeTraded(); const uiFeePercentage = 0.001; // 0.1% return totalVolume * uiFeePercentage; }; // Calculate recommended cooldown based on positions that fail after a win const getCooldownRecommendations = () => { if (positionsArray.length < 2 || !candles || candles.length < 2) { return { percentile75: "0", average: "0", median: "0" }; } // Determine candle timeframe in milliseconds const candleTimeframeMs = new Date(candles[1].date).getTime() - new Date(candles[0].date).getTime(); const sortedPositions = [...positionsArray].sort((a: Position, b: Position) => { const dateA = new Date(a.Open.date).getTime(); const dateB = new Date(b.Open.date).getTime(); return dateA - dateB; }); const failAfterWinGaps: number[] = []; 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); failAfterWinGaps.push(gapInCandles); } } } } if (failAfterWinGaps.length === 0) { return { percentile75: "0", average: "0", median: "0" }; } // Calculate the 75th percentile const sortedGaps = [...failAfterWinGaps].sort((a: number, b: number) => a - b); const percentile75Index = Math.floor(sortedGaps.length * 0.75); const percentile75 = sortedGaps[percentile75Index] || 0; // Calculate the average const sum = failAfterWinGaps.reduce((acc: number, gap: number) => acc + gap, 0); const average = sum / failAfterWinGaps.length; // Calculate the median const mid = Math.floor(sortedGaps.length / 2); const median = sortedGaps.length % 2 === 0 ? (sortedGaps[mid - 1] + sortedGaps[mid]) / 2 : sortedGaps[mid]; return { percentile75: Math.ceil(percentile75).toString(), average: Math.ceil(average).toString(), median: Math.ceil(median).toString() }; }; const cooldownRecommendations = getCooldownRecommendations(); // Calculate average trades per day const getAverageTradesPerDay = (): string => { if (positionsArray.length === 0) return "0.00"; // Get all trade dates and sort them const tradeDates = positionsArray.map((position: Position) => new Date(position.Open.date)).sort((a: Date, b: Date) => a.getTime() - b.getTime()); if (tradeDates.length < 2) return positionsArray.length.toString(); // Calculate the date range in days const firstTradeDate = tradeDates[0]; const lastTradeDate = tradeDates[tradeDates.length - 1]; const diffInMs = lastTradeDate.getTime() - firstTradeDate.getTime(); const diffInDays = Math.max(1, diffInMs / (1000 * 60 * 60 * 24)); // Ensure at least 1 day const averageTradesPerDay = positionsArray.length / diffInDays; return averageTradesPerDay.toFixed(2); }; return ( <>
{(isLoadingCandles || isLoadingFullBacktest) && (
{isLoadingFullBacktest ? "Loading backtest data..." : "Loading candles with indicators..."}
)}
{ const realized = p.ProfitAndLoss?.realized ?? 0 return realized > 0 ? p : null })} > { const realized = p.ProfitAndLoss?.realized ?? 0 return realized <= 0 ? p : null })} >
{!isLoadingCandles && !isLoadingFullBacktest && (
)}
) } export default BacktestRowDetails