Improve simulation marker UX: remove alert, adjust spacing, add hover tooltips
This commit is contained in:
@ -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');
|
||||
|
||||
Reference in New Issue
Block a user