Improve simulation marker UX: remove alert, adjust spacing, add hover tooltips

This commit is contained in:
BTC Bot
2026-02-13 12:47:14 +01:00
parent 003ab43086
commit ad2b93ba39
2 changed files with 186 additions and 36 deletions

View File

@ -869,7 +869,7 @@
};
}
run(candlesMap, strategyConfig, riskConfig) {
run(candlesMap, strategyConfig, riskConfig, simulationStart) {
const primaryTF = strategyConfig.timeframes?.primary || '1d';
const candles = candlesMap[primaryTF];
if (!candles) return { error: `No candles for primary timeframe ${primaryTF}` };
@ -894,19 +894,44 @@
const risk = new RiskManager(riskConfig);
const trades = [];
let position = null;
const startTimeMs = new Date(simulationStart).getTime();
// Optimized Pointer-based Timeframe Alignment
const pointers = {};
for (const tf in candlesMap) pointers[tf] = 0;
for (let i = 1; i < candles.length; i++) {
const time = new Date(candles[i].time).getTime();
const price = candles[i].close;
const time = candles[i].time;
const signal = this.evaluate(i, candles, candlesMap, indicatorResults, strategyConfig, position);
// Skip candles before simulation start (used for indicator warm-up)
if (time < startTimeMs) {
// Update pointers even for skipped candles
for (const tf in candlesMap) {
while (pointers[tf] < candlesMap[tf].length - 1 &&
new Date(candlesMap[tf][pointers[tf] + 1].time).getTime() <= time) {
pointers[tf]++;
}
}
continue;
}
// Update pointers to current time
for (const tf in candlesMap) {
while (pointers[tf] < candlesMap[tf].length - 1 &&
new Date(candlesMap[tf][pointers[tf] + 1].time).getTime() <= time) {
pointers[tf]++;
}
}
const signal = this.evaluate(i, pointers, candles, candlesMap, indicatorResults, strategyConfig, position);
if (signal === 'BUY' && !position) {
const size = risk.calculateSize(price);
position = { type: 'long', entryPrice: price, entryTime: time, size };
position = { type: 'long', entryPrice: price, entryTime: candles[i].time, size };
} else if (signal === 'SELL' && position) {
const pnl = (price - position.entryPrice) * position.size;
trades.push({ ...position, exitPrice: price, exitTime: time, pnl, pnlPct: (pnl / (position.entryPrice * position.size)) * 100 });
trades.push({ ...position, exitPrice: price, exitTime: candles[i].time, pnl, pnlPct: (pnl / (position.entryPrice * position.size)) * 100 });
risk.balance += pnl;
position = null;
}
@ -920,36 +945,20 @@
};
}
evaluate(index, candles, candlesMap, indicatorResults, config, position) {
evaluate(index, pointers, candles, candlesMap, indicatorResults, config, position) {
const primaryTF = config.timeframes?.primary || '1d';
const currentTime = new Date(candles[index].time).getTime();
// Helper to get indicator value at specific time for any TF
// Optimized getter using pointers
const getVal = (indName, tf) => {
const tfCandles = candlesMap[tf];
const tfValues = indicatorResults[tf][indName];
if (!tfCandles || !tfValues) return null;
// Find latest candle in TF that is <= currentTime
// Simple linear search for MVP, can be binary search
let tfIdx = -1;
for (let j = 0; j < tfCandles.length; j++) {
if (new Date(tfCandles[j].time).getTime() <= currentTime) tfIdx = j;
else break;
}
return tfIdx !== -1 ? tfValues[tfIdx] : null;
const tfValues = indicatorResults[tf]?.[indName];
if (!tfValues) return null;
return tfValues[pointers[tf]];
};
// Helper to get price at specific time for any TF
const getPrice = (tf) => {
const tfCandles = candlesMap[tf];
if (!tfCandles) return null;
let tfIdx = -1;
for (let j = 0; j < tfCandles.length; j++) {
if (new Date(tfCandles[j].time).getTime() <= currentTime) tfIdx = j;
else break;
}
return tfIdx !== -1 ? tfCandles[tfIdx].close : null;
return tfCandles[pointers[tf]].close;
};
// Simple logic for MVP strategies
@ -959,9 +968,7 @@
// Optional: Multi-TF trend filter
const secondaryTF = config.timeframes?.secondary?.[0];
const secondaryMA = secondaryTF ? getVal(`ma44_${secondaryTF}`, secondaryTF) : null;
const secondaryPrice = secondaryTF ? getPrice(secondaryTF) : null;
const trendOk = !secondaryTF || (secondaryPrice > secondaryMA);
const trendOk = !secondaryTF || (getPrice(secondaryTF) > getVal(`ma44_${secondaryTF}`, secondaryTF));
if (ma44) {
if (price > ma44 && trendOk) return 'BUY';
@ -982,8 +989,9 @@
const evaluateConditions = (conds) => {
if (!conds || !conds.conditions) return false;
const results = conds.conditions.map(c => {
const leftVal = c.indicator === 'price' ? getPrice(c.timeframe || primaryTF) : getVal(c.indicator, c.timeframe || primaryTF);
const rightVal = typeof c.value === 'number' ? c.value : (c.value === 'price' ? getPrice(c.timeframe || primaryTF) : getVal(c.value, c.timeframe || primaryTF));
const targetTF = c.timeframe || primaryTF;
const leftVal = c.indicator === 'price' ? getPrice(targetTF) : getVal(c.indicator, targetTF);
const rightVal = typeof c.value === 'number' ? c.value : (c.value === 'price' ? getPrice(targetTF) : getVal(c.value, targetTF));
if (leftVal === null || rightVal === null) return false;
@ -1428,6 +1436,10 @@
<span>Total P&L:</span>
<span id="simPnL" class="sim-value">--</span>
</div>
<div style="margin-top: 8px; display: flex; gap: 4px;">
<button class="ta-btn" style="flex: 1; padding: 4px; font-size: 10px;" onclick="showSimulationMarkers()">📍 Plot Signals</button>
<button class="ta-btn" style="flex: 1; padding: 4px; font-size: 10px;" onclick="clearSimulationMarkers()">🧹 Clear</button>
</div>
</div>
</div>
`;
@ -1626,11 +1638,14 @@
};
const engine = new ClientStrategyEngine();
const results = engine.run(candlesMap, strategyConfig, riskConfig);
const results = engine.run(candlesMap, strategyConfig, riskConfig, start);
if (results.error) throw new Error(results.error);
// 4. Display Results
// 4. Store Results for plotting
window.lastSimulationResults = results;
// 5. Display Results
displayBacktestResults({ results });
console.log(`Simulation complete: ${results.total_trades} trades found.`);
@ -1659,6 +1674,135 @@
document.getElementById('simResults').style.display = 'block';
}
// Setup marker tooltips on chart hover
function setupMarkerTooltips(trades) {
if (!window.dashboard || !window.dashboard.chart) return;
const chart = window.dashboard.chart;
const candleSeries = window.dashboard.candleSeries;
// Create tooltip element
let tooltip = document.getElementById('marker-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'marker-tooltip';
tooltip.style.cssText = `
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: #fff;
padding: 10px 14px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
display: none;
border: 1px solid #444;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
max-width: 280px;
line-height: 1.5;
`;
document.body.appendChild(tooltip);
}
// Track mouse movement
chart.subscribeCrosshairMove(param => {
if (!param.time || !param.point) {
tooltip.style.display = 'none';
return;
}
// Find nearby trade
const currentTime = param.time;
const nearbyTrade = trades.find(trade => {
const entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
const exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
// Check if within 5 bars
return Math.abs(currentTime - entryTime) <= 5 * 60 || Math.abs(currentTime - exitTime) <= 5 * 60;
});
if (nearbyTrade) {
const isEntry = Math.abs(currentTime - Math.floor(new Date(nearbyTrade.entryTime).getTime() / 1000)) <=
Math.abs(currentTime - Math.floor(new Date(nearbyTrade.exitTime).getTime() / 1000));
const pnlColor = nearbyTrade.pnl > 0 ? '#4caf50' : '#f44336';
const pnlSymbol = nearbyTrade.pnl > 0 ? '+' : '';
tooltip.innerHTML = `
<div style="font-weight: bold; margin-bottom: 6px; ${isEntry ? 'color: #26a69a;' : 'color: ' + pnlColor + ';'}">
${isEntry ? '🟢 BUY ENTRY' : '🔴 SELL EXIT'}
</div>
<div>Entry: $${nearbyTrade.entryPrice.toFixed(2)}</div>
<div>Exit: $${nearbyTrade.exitPrice.toFixed(2)}</div>
<div style="color: ${pnlColor}; font-weight: bold; margin-top: 4px;">
P&L: ${pnlSymbol}$${nearbyTrade.pnl.toFixed(2)} (${pnlSymbol}${nearbyTrade.pnlPct.toFixed(2)}%)
</div>
<div style="color: #888; font-size: 10px; margin-top: 4px;">
Duration: ${((new Date(nearbyTrade.exitTime) - new Date(nearbyTrade.entryTime)) / (1000 * 60)).toFixed(0)} min
</div>
`;
tooltip.style.left = (param.point.x + 15) + 'px';
tooltip.style.top = (param.point.y - 10) + 'px';
tooltip.style.display = 'block';
} else {
tooltip.style.display = 'none';
}
});
}
// Show simulation buy/sell markers on the chart
function showSimulationMarkers() {
if (!window.lastSimulationResults || !window.dashboard) return;
const trades = window.lastSimulationResults.trades;
const markers = [];
trades.forEach(trade => {
const entryTime = Math.floor(new Date(trade.entryTime).getTime() / 1000);
const exitTime = Math.floor(new Date(trade.exitTime).getTime() / 1000);
const pnlSymbol = trade.pnl > 0 ? '+' : '';
// Entry Marker - Buy signal
markers.push({
time: entryTime,
position: 'belowBar',
color: '#26a69a',
shape: 'arrowUp',
text: '🟢 BUY',
size: 2
});
// Exit Marker - Sell signal with P&L info
markers.push({
time: exitTime,
position: 'aboveBar',
color: trade.pnl > 0 ? '#26a69a' : '#ef5350',
shape: 'arrowDown',
text: (trade.pnl > 0 ? '🟢' : '🔴') + ' SELL ' + pnlSymbol + trade.pnlPct.toFixed(1) + '%',
size: 2
});
});
// Sort markers by time
markers.sort((a, b) => a.time - b.time);
// Apply to chart
window.dashboard.candleSeries.setMarkers(markers);
// Log to console instead of alert
console.log(`Plotted ${trades.length} trades on the chart.`);
// Show tooltip on marker hover via chart crosshair
setupMarkerTooltips(trades);
}
// Clear simulation markers
function clearSimulationMarkers() {
if (window.dashboard) {
window.dashboard.candleSeries.setMarkers([]);
}
}
// Set default start date (7 days ago)
function setDefaultStartDate() {
const startDateInput = document.getElementById('simStartDate');