From 2550f917e31532a9cb8aa9a7af0172f822b0890e Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 3 Jun 2025 02:16:52 +0700 Subject: [PATCH] Update front --- .../organism/Backtest/backtestRowDetails.tsx | 89 +++++++++ .../organism/Backtest/backtestTable.tsx | 178 +++++++++++++++++- 2 files changed, 257 insertions(+), 10 deletions(-) diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx index 0d9ef6f..dcb2850 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx @@ -149,6 +149,83 @@ const BacktestRowDetails: React.FC = ({ return totalVolume * uiFeePercentage; }; + // Calculate recommended cooldown based on positions that fail after a win + const getCooldownRecommendations = () => { + if (positions.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 = [...positions].sort((a, b) => { + 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, b) => a - b); + const percentile75Index = Math.floor(sortedGaps.length * 0.75); + const percentile75 = sortedGaps[percentile75Index] || 0; + + // Calculate the average + const sum = failAfterWinGaps.reduce((acc, gap) => 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(); + return ( <>
@@ -236,6 +313,18 @@ const BacktestRowDetails: React.FC = ({ maximumFractionDigits: 2 })} > + + +
diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index da2d085..b715295 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -18,6 +18,15 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest stopLoss: 0, takeProfit: 0, }) + const [positionTimingStats, setPositionTimingStats] = useState({ + averageOpenTime: 0, + medianOpenTime: 0, + losingPositionsAverageOpenTime: 0, + }) + const [cooldownRecommendations, setCooldownRecommendations] = useState({ + averageCooldown: 0, + medianCooldown: 0, + }) const [showBotNameModal, setShowBotNameModal] = useState(false) const [isForWatchOnly, setIsForWatchOnly] = useState(false) const [currentBacktest, setCurrentBacktest] = useState(null) @@ -314,6 +323,134 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest 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]) @@ -329,16 +466,37 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest ) : ( <> {list && list.length > 0 && ( -
- -
+ <> +
+ +
+
+ +
+
+ +
+ )}