Update front

This commit is contained in:
2025-06-03 02:16:52 +07:00
parent 8c2e9b59de
commit 2550f917e3
2 changed files with 257 additions and 10 deletions

View File

@@ -149,6 +149,83 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
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 (
<>
<div className="grid grid-flow-row">
@@ -236,6 +313,18 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
maximumFractionDigits: 2
})}
></CardText>
<CardText
title="Recommended Cooldown"
content={cooldownRecommendations.percentile75 + " candles"}
></CardText>
<CardText
title="Average Cooldown"
content={cooldownRecommendations.average + " candles"}
></CardText>
<CardText
title="Median Cooldown"
content={cooldownRecommendations.median + " candles"}
></CardText>
</div>
<div>
<figure>

View File

@@ -18,6 +18,15 @@ const BacktestTable: React.FC<IBacktestCards> = ({ 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<Backtest | null>(null)
@@ -314,6 +323,134 @@ const BacktestTable: React.FC<IBacktestCards> = ({ 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<IBacktestCards> = ({ list, isFetching, setBacktest
) : (
<>
{list && list.length > 0 && (
<div className="mb-4">
<CardText
title="Average Optimized Money Management"
content={
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
}
/>
</div>
<>
<div className="mb-4">
<CardText
title="Average Optimized Money Management"
content={
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
}
/>
</div>
<div className="mb-4">
<CardText
title="Position Timing Statistics"
content={
"Avg: " + positionTimingStats.averageOpenTime.toFixed(1) + "h | " +
"Median: " + positionTimingStats.medianOpenTime.toFixed(1) + "h | " +
"Losing Avg: " + positionTimingStats.losingPositionsAverageOpenTime.toFixed(1) + "h"
}
/>
</div>
<div className="mb-4">
<CardText
title="Cooldown Recommendations"
content={
"Avg: " + cooldownRecommendations.averageCooldown + " candles | " +
"Median: " + cooldownRecommendations.medianCooldown + " candles"
}
/>
</div>
</>
)}
<Table
columns={columns}