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:
DiTus
2026-03-03 13:15:29 +01:00
parent 73f325ce19
commit d92af6903d
13 changed files with 626 additions and 104 deletions

View File

@ -647,3 +647,53 @@
.right-sidebar.collapsed .sidebar-tabs { .right-sidebar.collapsed .sidebar-tabs {
display: none; display: none;
} }
/* Strategy Panel Styles */
.indicator-checklist {
max-height: 120px;
overflow-y: auto;
background: var(--tv-bg);
border: 1px solid var(--tv-border);
border-radius: 4px;
padding: 4px;
margin-top: 4px;
}
.indicator-checklist::-webkit-scrollbar {
width: 4px;
}
.indicator-checklist::-webkit-scrollbar-thumb {
background: var(--tv-border);
border-radius: 2px;
}
.checklist-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
border-radius: 3px;
}
.checklist-item:hover {
background: var(--tv-hover);
}
.checklist-item input {
cursor: pointer;
}
.equity-chart-container {
width: 100%;
height: 150px;
margin-top: 12px;
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--tv-border);
background: var(--tv-bg);
}
.results-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}

View File

@ -1480,6 +1480,7 @@
<div class="sidebar-header"> <div class="sidebar-header">
<div class="sidebar-tabs"> <div class="sidebar-tabs">
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button> <button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
<button class="sidebar-tab" data-tab="strategy">⚙️ Strategy</button>
</div> </div>
<button class="sidebar-toggle" id="sidebarToggleBtn"></button> <button class="sidebar-toggle" id="sidebarToggleBtn"></button>
</div> </div>
@ -1493,6 +1494,14 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Strategy Tab -->
<div class="sidebar-tab-panel" id="tab-strategy">
<div id="strategyPanel">
<!-- Content will be injected by strategy-panel.js -->
<div class="loading-strategies">Loading strategy tools...</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ import {
addIndicator, addIndicator,
removeIndicatorById removeIndicatorById
} from './ui/indicators-panel-new.js'; } from './ui/indicators-panel-new.js';
import { initStrategyPanel } from './ui/strategy-panel.js';
import { IndicatorRegistry } from './indicators/index.js'; import { IndicatorRegistry } from './indicators/index.js';
import { TimezoneConfig } from './config/timezone.js'; import { TimezoneConfig } from './config/timezone.js';
@ -75,6 +76,7 @@ document.addEventListener('DOMContentLoaded', async () => {
restoreSidebarTabState(); restoreSidebarTabState();
initSidebarTabs(); initSidebarTabs();
// Initialize indicator panel // Initialize panels
window.initIndicatorPanel(); window.initIndicatorPanel();
initStrategyPanel();
}); });

View File

@ -32,35 +32,38 @@ class BaseIndicator {
} }
// Signal calculation for Bollinger Bands // Signal calculation for Bollinger Bands
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values) { function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values, prevValues) {
const close = lastCandle.close; const close = lastCandle.close;
const prevClose = prevCandle?.close;
const upper = values?.upper; const upper = values?.upper;
const lower = values?.lower; const lower = values?.lower;
const middle = values?.middle; const prevUpper = prevValues?.upper;
const prevLower = prevValues?.lower;
if (!upper || !lower || !middle) { if (!upper || !lower || prevUpper === undefined || prevLower === undefined || prevClose === undefined) {
return null; return null;
} }
const bandwidth = (upper - lower) / middle * 100; // BUY: Price crosses DOWN through lower band (reversal/bounce play)
if (prevClose > prevLower && close <= lower) {
if (close <= lower) {
return { return {
type: SIGNAL_TYPES.BUY, type: SIGNAL_TYPES.BUY,
strength: Math.min(50 + (lower - close) / close * 1000, 100), strength: 70,
value: close, value: close,
reasoning: `Price (${close.toFixed(2)}) at or below lower band (${lower.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%` reasoning: `Price crossed DOWN through lower Bollinger Band`
}; };
} else if (close >= upper) { }
// SELL: Price crosses UP through upper band (overextended play)
else if (prevClose < prevUpper && close >= upper) {
return { return {
type: SIGNAL_TYPES.SELL, type: SIGNAL_TYPES.SELL,
strength: Math.min(50 + (close - upper) / close * 1000, 100), strength: 70,
value: close, value: close,
reasoning: `Price (${close.toFixed(2)}) at or above upper band (${upper.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%` reasoning: `Price crossed UP through upper Bollinger Band`
}; };
} else {
return null;
} }
return null;
} }
// Bollinger Bands Indicator class // Bollinger Bands Indicator class

View File

@ -126,35 +126,41 @@ function getMA(type, candles, period, source = 'close') {
} }
// Signal calculation for HTS // Signal calculation for HTS
function calculateHTSSignal(indicator, lastCandle, prevCandle, values) { function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) {
const fastHigh = values?.fastHigh;
const fastLow = values?.fastLow;
const slowHigh = values?.slowHigh;
const slowLow = values?.slowLow; const slowLow = values?.slowLow;
const slowHigh = values?.slowHigh;
const prevSlowLow = prevValues?.slowLow;
const prevSlowHigh = prevValues?.slowHigh;
if (!fastHigh || !fastLow || !slowHigh || !slowLow) { if (!slowLow || !slowHigh || !prevSlowLow || !prevSlowHigh) {
return null; return null;
} }
const close = lastCandle.close; const close = lastCandle.close;
const prevClose = prevCandle?.close;
if (close > slowLow) { if (prevClose === undefined) return null;
// BUY: Price crosses UP through slow low
if (prevClose <= prevSlowLow && close > slowLow) {
return { return {
type: SIGNAL_TYPES.BUY, type: SIGNAL_TYPES.BUY,
strength: Math.min(60 + (close - slowLow) / slowLow * 500, 100), strength: 85,
value: close, value: close,
reasoning: `Price (${close.toFixed(2)}) is above slow low (${slowLow.toFixed(2)})` reasoning: `Price crossed UP through slow low`
}; };
} else if (close < slowHigh) { }
// SELL: Price crosses DOWN through slow high
else if (prevClose >= prevSlowHigh && close < slowHigh) {
return { return {
type: SIGNAL_TYPES.SELL, type: SIGNAL_TYPES.SELL,
strength: Math.min(60 + (slowHigh - close) / close * 500, 100), strength: 85,
value: close, value: close,
reasoning: `Price (${close.toFixed(2)}) is below slow high (${slowHigh.toFixed(2)})` reasoning: `Price crossed DOWN through slow high`
}; };
} else {
return null;
} }
return null;
} }
// HTS Indicator class // HTS Indicator class

View File

@ -63,26 +63,30 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal
const prevClose = prevCandle?.close; const prevClose = prevCandle?.close;
const upper = values?.upper; const upper = values?.upper;
const lower = values?.lower; const lower = values?.lower;
const prevUpper = prevValues?.upper;
const prevLower = prevValues?.lower;
if (!upper || !lower || prevClose === undefined) { if (close === undefined || prevClose === undefined || !upper || !lower || !prevUpper || !prevLower) {
return null; return null;
} }
if (prevClose > lower && close < lower) { // BUY: Price crosses DOWN through lower Hurst Band
if (prevClose > prevLower && close <= lower) {
return { return {
type: 'buy', type: 'buy',
strength: 75, strength: 75,
value: close, value: close,
reasoning: `Price crossed down below lower Hurst Band (${lower.toFixed(2)}), expect bounce` reasoning: `Price crossed DOWN through lower Hurst Band`
}; };
} }
if (prevClose > upper && close < upper) { // SELL: Price crosses DOWN through upper Hurst Band (reversal from top)
if (prevClose > prevUpper && close <= upper) {
return { return {
type: 'sell', type: 'sell',
strength: 75, strength: 75,
value: close, value: close,
reasoning: `Price crossed down below upper Hurst Band (${upper.toFixed(2)}), expect reversal` reasoning: `Price crossed DOWN through upper Hurst Band`
}; };
} }

View File

@ -50,32 +50,37 @@ function calculateEMAInline(data, period) {
} }
// Signal calculation for MACD // Signal calculation for MACD
function calculateMACDSignal(indicator, lastCandle, prevCandle, values) { function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) {
const macd = values?.macd; const macd = values?.macd;
const signal = values?.signal; const signal = values?.signal;
const histogram = values?.histogram; const prevMacd = prevValues?.macd;
const prevSignal = prevValues?.signal;
if (!macd || macd === null || !signal || signal === null) { if (macd === undefined || macd === null || signal === undefined || signal === null ||
prevMacd === undefined || prevMacd === null || prevSignal === undefined || prevSignal === null) {
return null; return null;
} }
let signalType, strength, reasoning; // BUY: MACD crosses UP through Signal line
if (prevMacd <= prevSignal && macd > signal) {
const prevCandleHistogram = prevCandle ? values?.histogram : null; return {
type: SIGNAL_TYPES.BUY,
if (macd > signal) { strength: 80,
signalType = SIGNAL_TYPES.BUY; value: macd,
strength = Math.min(50 + histogram * 500, 100); reasoning: `MACD crossed UP through Signal line`
reasoning = `MACD (${macd.toFixed(2)}) is above Signal (${signal.toFixed(2)})`; };
} else if (macd < signal) { }
signalType = SIGNAL_TYPES.SELL; // SELL: MACD crosses DOWN through Signal line
strength = Math.min(50 + Math.abs(histogram) * 500, 100); else if (prevMacd >= prevSignal && macd < signal) {
reasoning = `MACD (${macd.toFixed(2)}) is below Signal (${signal.toFixed(2)})`; return {
} else { type: SIGNAL_TYPES.SELL,
return null; strength: 80,
value: macd,
reasoning: `MACD crossed DOWN through Signal line`
};
} }
return { type: signalType, strength, value: macd, reasoning }; return null;
} }
// MACD Indicator class // MACD Indicator class

View File

@ -114,31 +114,35 @@ function calculateVWMA(candles, period, source = 'close') {
} }
// Signal calculation for Moving Average // Signal calculation for Moving Average
function calculateMASignal(indicator, lastCandle, prevCandle, values) { function calculateMASignal(indicator, lastCandle, prevCandle, values, prevValues) {
const close = lastCandle.close; const close = lastCandle.close;
const prevClose = prevCandle?.close;
const ma = values?.ma; const ma = values?.ma;
const prevMa = prevValues?.ma;
if (!ma && ma !== 0) { if (!ma && ma !== 0) return null;
return null; if (prevClose === undefined || prevMa === undefined || prevMa === null) return null;
}
if (close > ma) { // BUY: Price crosses UP through MA
if (prevClose <= prevMa && close > ma) {
return { return {
type: SIGNAL_TYPES.BUY, type: SIGNAL_TYPES.BUY,
strength: Math.min(60 + ((close - ma) / ma) * 500, 100), strength: 80,
value: close, value: close,
reasoning: `Price (${close.toFixed(2)}) is above MA (${ma.toFixed(2)})` reasoning: `Price crossed UP through MA`
}; };
} else if (close < ma) { }
// SELL: Price crosses DOWN through MA
else if (prevClose >= prevMa && close < ma) {
return { return {
type: SIGNAL_TYPES.SELL, type: SIGNAL_TYPES.SELL,
strength: Math.min(60 + ((ma - close) / ma) * 500, 100), strength: 80,
value: close, value: close,
reasoning: `Price (${close.toFixed(2)}) is below MA (${ma.toFixed(2)})` reasoning: `Price crossed DOWN through MA`
}; };
} else {
return null;
} }
return null;
} }
// MA Indicator class // MA Indicator class

View File

@ -38,42 +38,30 @@ function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValue
const overbought = indicator.params?.overbought || 70; const overbought = indicator.params?.overbought || 70;
const oversold = indicator.params?.oversold || 30; const oversold = indicator.params?.oversold || 30;
if (!rsi || rsi === null) { if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === null) {
return null; return null;
} }
let signalType, strength, reasoning; // BUY when RSI crosses UP through oversold level
if (prevRsi < oversold && rsi >= oversold) {
return {
type: SIGNAL_TYPES.BUY,
strength: 75,
value: rsi,
reasoning: `RSI crossed UP through oversold level (${oversold})`
};
}
// SELL when RSI crosses DOWN through overbought level
else if (prevRsi > overbought && rsi <= overbought) {
return {
type: SIGNAL_TYPES.SELL,
strength: 75,
value: rsi,
reasoning: `RSI crossed DOWN through overbought level (${overbought})`
};
}
// BUY when RSI crosses UP through oversold band (bottom band)
// RSI was below oversold, now above oversold
if (prevRsi !== undefined && prevRsi !== null && prevRsi < oversold && rsi >= oversold) {
signalType = SIGNAL_TYPES.BUY;
strength = Math.min(50 + (rsi - oversold) * 2, 100);
reasoning = `RSI (${rsi.toFixed(2)}) crossed up through oversold level (${oversold})`;
}
// SELL when RSI crosses DOWN through overbought band (top band)
// RSI was above overbought, now below overbought
else if (prevRsi !== undefined && prevRsi !== null && prevRsi > overbought && rsi <= overbought) {
signalType = SIGNAL_TYPES.SELL;
strength = Math.min(50 + (overbought - rsi) * 2, 100);
reasoning = `RSI (${rsi.toFixed(2)}) crossed down through overbought level (${overbought})`;
}
// When RSI is in oversold territory but no crossover - strong BUY
else if (rsi < oversold) {
signalType = SIGNAL_TYPES.BUY;
strength = Math.min(40 + (oversold - rsi) * 1.5, 80);
reasoning = `RSI (${rsi.toFixed(2)}) is oversold (<${oversold})`;
}
// When RSI is in overbought territory but no crossover - strong SELL
else if (rsi > overbought) {
signalType = SIGNAL_TYPES.SELL;
strength = Math.min(40 + (rsi - overbought) * 1.5, 80);
reasoning = `RSI (${rsi.toFixed(2)}) is overbought (>${overbought})`;
} else {
return null; return null;
}
return { type: signalType, strength, value: rsi, reasoning };
} }
// RSI Indicator class // RSI Indicator class

View File

@ -32,33 +32,38 @@ class BaseIndicator {
} }
// Signal calculation for Stochastic // Signal calculation for Stochastic
function calculateStochSignal(indicator, lastCandle, prevCandle, values) { function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) {
const k = values?.k; const k = values?.k;
const d = values?.d; const d = values?.d;
const prevK = prevValues?.k;
const prevD = prevValues?.d;
const overbought = indicator.params?.overbought || 80; const overbought = indicator.params?.overbought || 80;
const oversold = indicator.params?.oversold || 20; const oversold = indicator.params?.oversold || 20;
if (!k || !d) { if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) {
return null; return null;
} }
if (k < oversold && d < oversold) { // BUY: %K crosses UP through %D while both are oversold
if (prevK <= prevD && k > d && k < oversold) {
return { return {
type: SIGNAL_TYPES.BUY, type: SIGNAL_TYPES.BUY,
strength: Math.min(50 + (oversold - k) * 2, 100), strength: 80,
value: k, value: k,
reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) oversold (<${oversold})` reasoning: `Stochastic %K crossed UP through %D in oversold zone`
}; };
} else if (k > overbought && d > overbought) { }
// SELL: %K crosses DOWN through %D while both are overbought
else if (prevK >= prevD && k < d && k > overbought) {
return { return {
type: SIGNAL_TYPES.SELL, type: SIGNAL_TYPES.SELL,
strength: Math.min(50 + (k - overbought) * 2, 100), strength: 80,
value: k, value: k,
reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) overbought (>${overbought})` reasoning: `Stochastic %K crossed DOWN through %D in overbought zone`
}; };
} else {
return null;
} }
return null;
} }
// Stochastic Oscillator Indicator class // Stochastic Oscillator Indicator class

View File

@ -21,10 +21,21 @@ constructor() {
this.indicatorSignals = []; this.indicatorSignals = [];
this.summarySignal = null; this.summarySignal = null;
this.lastCandleTimestamp = null; this.lastCandleTimestamp = null;
this.simulationMarkers = [];
this.init(); this.init();
} }
setSimulationMarkers(markers) {
this.simulationMarkers = markers || [];
this.updateSignalMarkers();
}
clearSimulationMarkers() {
this.simulationMarkers = [];
this.updateSignalMarkers();
}
init() { init() {
this.createTimeframeButtons(); this.createTimeframeButtons();
this.initChart(); this.initChart();
@ -584,11 +595,18 @@ async loadSignals() {
} }
} }
updateSignalMarkers() { updateSignalMarkers() {
const candles = this.allData.get(this.currentInterval); const candles = this.allData.get(this.currentInterval);
if (!candles || candles.length === 0) return; 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 we have a marker controller, update markers through it
if (this.markerController) { if (this.markerController) {

View File

@ -54,6 +54,12 @@ export function switchTab(tabId) {
window.drawIndicatorsOnChart(); window.drawIndicatorsOnChart();
} }
}, 50); }, 50);
} else if (tabId === 'strategy') {
setTimeout(() => {
if (window.renderStrategyPanel) {
window.renderStrategyPanel();
}
}, 50);
} }
} }

View 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();
};