Update front
This commit is contained in:
@@ -149,6 +149,83 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
|||||||
return totalVolume * uiFeePercentage;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-flow-row">
|
<div className="grid grid-flow-row">
|
||||||
@@ -236,6 +313,18 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
|||||||
maximumFractionDigits: 2
|
maximumFractionDigits: 2
|
||||||
})}
|
})}
|
||||||
></CardText>
|
></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>
|
||||||
<div>
|
<div>
|
||||||
<figure>
|
<figure>
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
stopLoss: 0,
|
stopLoss: 0,
|
||||||
takeProfit: 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 [showBotNameModal, setShowBotNameModal] = useState(false)
|
||||||
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
|
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
|
||||||
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
|
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
|
||||||
@@ -314,6 +323,134 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
stopLoss: stopLoss / optimized.length,
|
stopLoss: stopLoss / optimized.length,
|
||||||
takeProfit: takeProfit / 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])
|
}, [list])
|
||||||
@@ -329,16 +466,37 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{list && list.length > 0 && (
|
{list && list.length > 0 && (
|
||||||
<div className="mb-4">
|
<>
|
||||||
<CardText
|
<div className="mb-4">
|
||||||
title="Average Optimized Money Management"
|
<CardText
|
||||||
content={
|
title="Average Optimized Money Management"
|
||||||
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
|
content={
|
||||||
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
|
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
|
||||||
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
|
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
|
||||||
}
|
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
|
||||||
/>
|
}
|
||||||
</div>
|
/>
|
||||||
|
</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
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
Reference in New Issue
Block a user