Update front
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user