Implement Strategy tab with Ping-Pong backtesting and crossover-based signal logic
- Add 'Strategy' tab to sidebar for backtesting simulations - Create strategy-panel.js for Ping-Pong and Accumulation mode simulations - Refactor all indicators (MA, HTS, RSI, MACD, BB, STOCH, Hurst) to use strict crossover-based signal calculation - Update chart.js with setSimulationMarkers and clearSimulationMarkers support - Implement single-entry rule in Ping-Pong simulation mode
This commit is contained in:
@ -21,9 +21,20 @@ constructor() {
|
||||
this.indicatorSignals = [];
|
||||
this.summarySignal = null;
|
||||
this.lastCandleTimestamp = null;
|
||||
this.simulationMarkers = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
setSimulationMarkers(markers) {
|
||||
this.simulationMarkers = markers || [];
|
||||
this.updateSignalMarkers();
|
||||
}
|
||||
|
||||
clearSimulationMarkers() {
|
||||
this.simulationMarkers = [];
|
||||
this.updateSignalMarkers();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createTimeframeButtons();
|
||||
@ -584,11 +595,18 @@ async loadSignals() {
|
||||
}
|
||||
}
|
||||
|
||||
updateSignalMarkers() {
|
||||
updateSignalMarkers() {
|
||||
const candles = this.allData.get(this.currentInterval);
|
||||
if (!candles || candles.length === 0) return;
|
||||
|
||||
const markers = calculateSignalMarkers(candles);
|
||||
let markers = calculateSignalMarkers(candles);
|
||||
|
||||
// Merge simulation markers if present
|
||||
if (this.simulationMarkers && this.simulationMarkers.length > 0) {
|
||||
markers = [...markers, ...this.simulationMarkers];
|
||||
// Re-sort combined markers by time
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
// If we have a marker controller, update markers through it
|
||||
if (this.markerController) {
|
||||
|
||||
@ -54,6 +54,12 @@ export function switchTab(tabId) {
|
||||
window.drawIndicatorsOnChart();
|
||||
}
|
||||
}, 50);
|
||||
} else if (tabId === 'strategy') {
|
||||
setTimeout(() => {
|
||||
if (window.renderStrategyPanel) {
|
||||
window.renderStrategyPanel();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
422
src/api/dashboard/static/js/ui/strategy-panel.js
Normal file
422
src/api/dashboard/static/js/ui/strategy-panel.js
Normal file
@ -0,0 +1,422 @@
|
||||
import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js';
|
||||
|
||||
let activeIndicators = [];
|
||||
let simulationResults = null;
|
||||
let equitySeries = null;
|
||||
let equityChart = null;
|
||||
|
||||
export function initStrategyPanel() {
|
||||
window.renderStrategyPanel = renderStrategyPanel;
|
||||
renderStrategyPanel();
|
||||
|
||||
// Listen for indicator changes to update the signal selection list
|
||||
const originalAddIndicator = window.addIndicator;
|
||||
window.addIndicator = function(...args) {
|
||||
const res = originalAddIndicator.apply(this, args);
|
||||
setTimeout(renderStrategyPanel, 100);
|
||||
return res;
|
||||
};
|
||||
|
||||
const originalRemoveIndicator = window.removeIndicatorById;
|
||||
window.removeIndicatorById = function(...args) {
|
||||
const res = originalRemoveIndicator.apply(this, args);
|
||||
setTimeout(renderStrategyPanel, 100);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
export function renderStrategyPanel() {
|
||||
const container = document.getElementById('strategyPanel');
|
||||
if (!container) return;
|
||||
|
||||
activeIndicators = window.getActiveIndicators?.() || [];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header">
|
||||
<span>⚙️</span> Ping-Pong Strategy
|
||||
</div>
|
||||
<div class="sidebar-section-content">
|
||||
<div class="sim-input-group">
|
||||
<label>Start Date & Time</label>
|
||||
<input type="datetime-local" id="simStartDate" class="sim-input" value="2026-01-01T00:00">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Direction</label>
|
||||
<select id="simDirection" class="sim-input">
|
||||
<option value="long" selected>Long</option>
|
||||
<option value="short">Short</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Initial Capital ($)</label>
|
||||
<input type="number" id="simCapital" class="sim-input" value="10000" min="1">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Leverage</label>
|
||||
<input type="number" id="simLeverage" class="sim-input" value="1" min="1" max="100">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Position Size ($)</label>
|
||||
<input type="number" id="simPosSize" class="sim-input" value="10" min="1">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Take Profit (%)</label>
|
||||
<input type="number" id="simTP" class="sim-input" value="15" step="0.1" min="0.1">
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Open Signal Indicators</label>
|
||||
<div class="indicator-checklist" id="openSignalsList">
|
||||
${renderIndicatorChecklist('open')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-input-group">
|
||||
<label>Close Signal Indicators (Empty = Accumulation)</label>
|
||||
<div class="indicator-checklist" id="closeSignalsList">
|
||||
${renderIndicatorChecklist('close')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="sim-run-btn" id="runSimulationBtn">Run Simulation</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="simulationResults" class="sim-results" style="display: none;">
|
||||
<!-- Results will be injected here -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('runSimulationBtn').addEventListener('click', runSimulation);
|
||||
}
|
||||
|
||||
function renderIndicatorChecklist(prefix) {
|
||||
if (activeIndicators.length === 0) {
|
||||
return '<div style="padding: 8px; color: var(--tv-text-secondary); font-size: 11px;">No active indicators on chart</div>';
|
||||
}
|
||||
|
||||
return activeIndicators.map(ind => `
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" data-id="${ind.id}" class="sim-${prefix}-check">
|
||||
<span>${ind.name}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function runSimulation() {
|
||||
const btn = document.getElementById('runSimulationBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Simulating...';
|
||||
|
||||
try {
|
||||
const config = {
|
||||
startDate: new Date(document.getElementById('simStartDate').value).getTime() / 1000,
|
||||
direction: document.getElementById('simDirection').value,
|
||||
capital: parseFloat(document.getElementById('simCapital').value),
|
||||
leverage: parseFloat(document.getElementById('simLeverage').value),
|
||||
posSize: parseFloat(document.getElementById('simPosSize').value),
|
||||
tp: parseFloat(document.getElementById('simTP').value) / 100,
|
||||
openIndicators: Array.from(document.querySelectorAll('.sim-open-check:checked')).map(el => el.dataset.id),
|
||||
closeIndicators: Array.from(document.querySelectorAll('.sim-close-check:checked')).map(el => el.dataset.id)
|
||||
};
|
||||
|
||||
if (config.openIndicators.length === 0) {
|
||||
alert('Please choose at least one indicator for opening positions.');
|
||||
return;
|
||||
}
|
||||
|
||||
const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval);
|
||||
if (!candles || candles.length === 0) {
|
||||
alert('No candle data available.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter candles by start date
|
||||
const simCandles = candles.filter(c => c.time >= config.startDate);
|
||||
if (simCandles.length === 0) {
|
||||
alert('No data available for the selected start date.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate all indicator values and signals for the sim period
|
||||
const indicatorSignals = {}; // { indicatorId: [signals per candle] }
|
||||
|
||||
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
|
||||
const ind = activeIndicators.find(a => a.id === indId);
|
||||
const IndicatorClass = IndicatorRegistry[ind.type];
|
||||
const signalFunc = getSignalFunction(ind.type);
|
||||
|
||||
if (IndicatorClass && signalFunc) {
|
||||
const instance = new IndicatorClass(ind);
|
||||
const results = instance.calculate(candles); // Calculate on FULL history for correctness
|
||||
|
||||
// Map full history results to simCandles indices
|
||||
const simSignals = simCandles.map(candle => {
|
||||
const idx = candles.findIndex(c => c.time === candle.time);
|
||||
if (idx < 1) return null;
|
||||
|
||||
const res = results[idx];
|
||||
const prevRes = results[idx-1];
|
||||
const values = typeof res === 'object' ? res : { ma: res };
|
||||
const prevValues = typeof prevRes === 'object' ? prevRes : { ma: prevRes };
|
||||
|
||||
return signalFunc(ind, candles[idx], candles[idx-1], values, prevValues);
|
||||
});
|
||||
|
||||
indicatorSignals[indId] = simSignals;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulation loop
|
||||
let balance = config.capital;
|
||||
let equity = [{ time: simCandles[0].time, value: balance }];
|
||||
let positions = []; // { entryPrice, size, type, entryTime }
|
||||
let trades = []; // { type, entryTime, exitTime, entryPrice, exitPrice, pnl, result }
|
||||
|
||||
for (let i = 0; i < simCandles.length; i++) {
|
||||
const candle = simCandles[i];
|
||||
const price = candle.close;
|
||||
|
||||
// 1. Check TP for existing positions
|
||||
for (let j = positions.length - 1; j >= 0; j--) {
|
||||
const pos = positions[j];
|
||||
let isClosed = false;
|
||||
let exitPrice = price;
|
||||
let reason = '';
|
||||
|
||||
// TP Logic
|
||||
if (pos.type === 'long') {
|
||||
if (candle.high >= pos.entryPrice * (1 + config.tp)) {
|
||||
isClosed = true;
|
||||
exitPrice = pos.entryPrice * (1 + config.tp);
|
||||
reason = 'TP';
|
||||
}
|
||||
} else {
|
||||
if (candle.low <= pos.entryPrice * (1 - config.tp)) {
|
||||
isClosed = true;
|
||||
exitPrice = pos.entryPrice * (1 - config.tp);
|
||||
reason = 'TP';
|
||||
}
|
||||
}
|
||||
|
||||
// Close Signal Logic
|
||||
if (!isClosed && config.closeIndicators.length > 0) {
|
||||
const hasCloseSignal = config.closeIndicators.some(id => {
|
||||
const sig = indicatorSignals[id][i];
|
||||
if (!sig) return false;
|
||||
|
||||
// Short: logic is inverted
|
||||
if (config.direction === 'long') {
|
||||
return sig.type === 'sell'; // Sell signal closes long
|
||||
} else {
|
||||
return sig.type === 'buy'; // Buy signal closes short
|
||||
}
|
||||
});
|
||||
|
||||
if (hasCloseSignal) {
|
||||
isClosed = true;
|
||||
reason = 'Signal';
|
||||
}
|
||||
}
|
||||
|
||||
if (isClosed) {
|
||||
const pnl = pos.type === 'long'
|
||||
? (exitPrice - pos.entryPrice) / pos.entryPrice * pos.size * config.leverage
|
||||
: (pos.entryPrice - exitPrice) / pos.entryPrice * pos.size * config.leverage;
|
||||
|
||||
balance += pnl;
|
||||
trades.push({
|
||||
type: pos.type,
|
||||
entryTime: pos.entryTime,
|
||||
exitTime: candle.time,
|
||||
entryPrice: pos.entryPrice,
|
||||
exitPrice: exitPrice,
|
||||
pnl: pnl,
|
||||
reason: reason
|
||||
});
|
||||
positions.splice(j, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check Open Signals
|
||||
const hasOpenSignal = config.openIndicators.some(id => {
|
||||
const sig = indicatorSignals[id][i];
|
||||
if (!sig) return false;
|
||||
|
||||
if (config.direction === 'long') {
|
||||
return sig.type === 'buy';
|
||||
} else {
|
||||
return sig.type === 'sell';
|
||||
}
|
||||
});
|
||||
|
||||
// Ping-Pong Mode: Only 1 active position allowed
|
||||
// Accumulation Mode (no close indicators): Multiple positions allowed
|
||||
const isAccumulation = config.closeIndicators.length === 0;
|
||||
const canOpen = isAccumulation || positions.length === 0;
|
||||
|
||||
if (hasOpenSignal && canOpen && balance >= config.posSize) {
|
||||
positions.push({
|
||||
type: config.direction,
|
||||
entryPrice: price,
|
||||
size: config.posSize,
|
||||
entryTime: candle.time
|
||||
});
|
||||
}
|
||||
|
||||
equity.push({ time: candle.time, value: balance });
|
||||
}
|
||||
|
||||
displayResults(trades, equity, config);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Simulation] Error:', error);
|
||||
alert('Simulation failed. See console for details.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Run Simulation';
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(trades, equity, config) {
|
||||
const resultsDiv = document.getElementById('simulationResults');
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
const totalTrades = trades.length;
|
||||
const profitableTrades = trades.filter(t => t.pnl > 0).length;
|
||||
const winRate = totalTrades > 0 ? (profitableTrades / totalTrades * 100).toFixed(1) : 0;
|
||||
const totalPnl = trades.reduce((sum, t) => sum + t.pnl, 0);
|
||||
const finalBalance = config.capital + totalPnl;
|
||||
const roi = (totalPnl / config.capital * 100).toFixed(2);
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header">Results</div>
|
||||
<div class="sidebar-section-content">
|
||||
<div class="results-summary">
|
||||
<div class="result-stat">
|
||||
<div class="result-stat-value ${totalPnl >= 0 ? 'positive' : 'negative'}">${roi}%</div>
|
||||
<div class="result-stat-label">ROI</div>
|
||||
</div>
|
||||
<div class="result-stat">
|
||||
<div class="result-stat-value">${winRate}%</div>
|
||||
<div class="result-stat-label">Win Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-stat-row">
|
||||
<span>Total Trades</span>
|
||||
<span class="sim-value">${totalTrades}</span>
|
||||
</div>
|
||||
<div class="sim-stat-row">
|
||||
<span>Profit/Loss</span>
|
||||
<span class="sim-value ${totalPnl >= 0 ? 'positive' : 'negative'}">$${totalPnl.toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="sim-stat-row">
|
||||
<span>Final Balance</span>
|
||||
<span class="sim-value">$${finalBalance.toFixed(2)}</span>
|
||||
</div>
|
||||
|
||||
<div class="equity-chart-container" id="equityChart"></div>
|
||||
|
||||
<div class="results-actions">
|
||||
<button class="action-btn secondary" id="toggleTradeMarkers">Show Markers</button>
|
||||
<button class="action-btn secondary" id="clearSim">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create Equity Chart
|
||||
setTimeout(() => {
|
||||
const chartContainer = document.getElementById('equityChart');
|
||||
if (!chartContainer) return;
|
||||
|
||||
equityChart = LightweightCharts.createChart(chartContainer, {
|
||||
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||
rightPriceScale: { borderColor: '#2a2e39' },
|
||||
timeScale: { borderColor: '#2a2e39', visible: false },
|
||||
handleScroll: false,
|
||||
handleScale: false
|
||||
});
|
||||
|
||||
equitySeries = equityChart.addSeries(LightweightCharts.AreaSeries, {
|
||||
lineColor: totalPnl >= 0 ? '#26a69a' : '#ef5350',
|
||||
topColor: totalPnl >= 0 ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)',
|
||||
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||
lineWidth: 2,
|
||||
});
|
||||
|
||||
equitySeries.setData(equity);
|
||||
equityChart.timeScale().fitContent();
|
||||
}, 100);
|
||||
|
||||
document.getElementById('toggleTradeMarkers').addEventListener('click', () => {
|
||||
toggleSimulationMarkers(trades);
|
||||
});
|
||||
|
||||
document.getElementById('clearSim').addEventListener('click', () => {
|
||||
resultsDiv.style.display = 'none';
|
||||
clearSimulationMarkers();
|
||||
});
|
||||
}
|
||||
|
||||
let tradeMarkers = [];
|
||||
|
||||
function toggleSimulationMarkers(trades) {
|
||||
if (tradeMarkers.length > 0) {
|
||||
clearSimulationMarkers();
|
||||
document.getElementById('toggleTradeMarkers').textContent = 'Show Markers';
|
||||
return;
|
||||
}
|
||||
|
||||
const markers = [];
|
||||
trades.forEach(t => {
|
||||
// Entry marker
|
||||
markers.push({
|
||||
time: t.entryTime,
|
||||
position: t.type === 'long' ? 'belowBar' : 'aboveBar',
|
||||
color: t.type === 'long' ? '#2962ff' : '#9c27b0',
|
||||
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
|
||||
text: `Entry ${t.type.toUpperCase()}`
|
||||
});
|
||||
|
||||
// Exit marker
|
||||
markers.push({
|
||||
time: t.exitTime,
|
||||
position: t.type === 'long' ? 'aboveBar' : 'belowBar',
|
||||
color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
|
||||
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
|
||||
text: `Exit ${t.reason} ($${t.pnl.toFixed(2)})`
|
||||
});
|
||||
});
|
||||
|
||||
// Sort markers by time
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
|
||||
if (window.dashboard) {
|
||||
window.dashboard.setSimulationMarkers(markers);
|
||||
tradeMarkers = markers;
|
||||
document.getElementById('toggleTradeMarkers').textContent = 'Hide Markers';
|
||||
}
|
||||
}
|
||||
|
||||
function clearSimulationMarkers() {
|
||||
if (window.dashboard) {
|
||||
window.dashboard.clearSimulationMarkers();
|
||||
tradeMarkers = [];
|
||||
}
|
||||
}
|
||||
|
||||
window.clearSimulationResults = function() {
|
||||
const resultsDiv = document.getElementById('simulationResults');
|
||||
if (resultsDiv) resultsDiv.style.display = 'none';
|
||||
clearSimulationMarkers();
|
||||
};
|
||||
Reference in New Issue
Block a user