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:
@ -647,3 +647,53 @@
|
||||
.right-sidebar.collapsed .sidebar-tabs {
|
||||
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;
|
||||
}
|
||||
@ -1480,6 +1480,7 @@
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-tabs">
|
||||
<button class="sidebar-tab active" data-tab="indicators">📊 Indicators</button>
|
||||
<button class="sidebar-tab" data-tab="strategy">⚙️ Strategy</button>
|
||||
</div>
|
||||
<button class="sidebar-toggle" id="sidebarToggleBtn">◀</button>
|
||||
</div>
|
||||
@ -1493,6 +1494,14 @@
|
||||
</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>
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
addIndicator,
|
||||
removeIndicatorById
|
||||
} from './ui/indicators-panel-new.js';
|
||||
import { initStrategyPanel } from './ui/strategy-panel.js';
|
||||
import { IndicatorRegistry } from './indicators/index.js';
|
||||
import { TimezoneConfig } from './config/timezone.js';
|
||||
|
||||
@ -75,6 +76,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
restoreSidebarTabState();
|
||||
initSidebarTabs();
|
||||
|
||||
// Initialize indicator panel
|
||||
// Initialize panels
|
||||
window.initIndicatorPanel();
|
||||
initStrategyPanel();
|
||||
});
|
||||
|
||||
@ -32,35 +32,38 @@ class BaseIndicator {
|
||||
}
|
||||
|
||||
// Signal calculation for Bollinger Bands
|
||||
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const close = lastCandle.close;
|
||||
const prevClose = prevCandle?.close;
|
||||
const upper = values?.upper;
|
||||
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;
|
||||
}
|
||||
|
||||
const bandwidth = (upper - lower) / middle * 100;
|
||||
|
||||
if (close <= lower) {
|
||||
// BUY: Price crosses DOWN through lower band (reversal/bounce play)
|
||||
if (prevClose > prevLower && close <= lower) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(50 + (lower - close) / close * 1000, 100),
|
||||
strength: 70,
|
||||
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 {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(50 + (close - upper) / close * 1000, 100),
|
||||
strength: 70,
|
||||
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
|
||||
|
||||
@ -126,35 +126,41 @@ function getMA(type, candles, period, source = 'close') {
|
||||
}
|
||||
|
||||
// Signal calculation for HTS
|
||||
function calculateHTSSignal(indicator, lastCandle, prevCandle, values) {
|
||||
const fastHigh = values?.fastHigh;
|
||||
const fastLow = values?.fastLow;
|
||||
const slowHigh = values?.slowHigh;
|
||||
function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(60 + (close - slowLow) / slowLow * 500, 100),
|
||||
strength: 85,
|
||||
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 {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(60 + (slowHigh - close) / close * 500, 100),
|
||||
strength: 85,
|
||||
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
|
||||
|
||||
@ -63,26 +63,30 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal
|
||||
const prevClose = prevCandle?.close;
|
||||
const upper = values?.upper;
|
||||
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;
|
||||
}
|
||||
|
||||
if (prevClose > lower && close < lower) {
|
||||
// BUY: Price crosses DOWN through lower Hurst Band
|
||||
if (prevClose > prevLower && close <= lower) {
|
||||
return {
|
||||
type: 'buy',
|
||||
strength: 75,
|
||||
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 {
|
||||
type: 'sell',
|
||||
strength: 75,
|
||||
value: close,
|
||||
reasoning: `Price crossed down below upper Hurst Band (${upper.toFixed(2)}), expect reversal`
|
||||
reasoning: `Price crossed DOWN through upper Hurst Band`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -50,32 +50,37 @@ function calculateEMAInline(data, period) {
|
||||
}
|
||||
|
||||
// Signal calculation for MACD
|
||||
function calculateMACDSignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const macd = values?.macd;
|
||||
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;
|
||||
}
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
const prevCandleHistogram = prevCandle ? values?.histogram : null;
|
||||
|
||||
if (macd > signal) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(50 + histogram * 500, 100);
|
||||
reasoning = `MACD (${macd.toFixed(2)}) is above Signal (${signal.toFixed(2)})`;
|
||||
} else if (macd < signal) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(50 + Math.abs(histogram) * 500, 100);
|
||||
reasoning = `MACD (${macd.toFixed(2)}) is below Signal (${signal.toFixed(2)})`;
|
||||
} else {
|
||||
return null;
|
||||
// BUY: MACD crosses UP through Signal line
|
||||
if (prevMacd <= prevSignal && macd > signal) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: 80,
|
||||
value: macd,
|
||||
reasoning: `MACD crossed UP through Signal line`
|
||||
};
|
||||
}
|
||||
// SELL: MACD crosses DOWN through Signal line
|
||||
else if (prevMacd >= prevSignal && macd < signal) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: 80,
|
||||
value: macd,
|
||||
reasoning: `MACD crossed DOWN through Signal line`
|
||||
};
|
||||
}
|
||||
|
||||
return { type: signalType, strength, value: macd, reasoning };
|
||||
return null;
|
||||
}
|
||||
|
||||
// MACD Indicator class
|
||||
|
||||
@ -114,31 +114,35 @@ function calculateVWMA(candles, period, source = 'close') {
|
||||
}
|
||||
|
||||
// Signal calculation for Moving Average
|
||||
function calculateMASignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateMASignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const close = lastCandle.close;
|
||||
const prevClose = prevCandle?.close;
|
||||
const ma = values?.ma;
|
||||
const prevMa = prevValues?.ma;
|
||||
|
||||
if (!ma && ma !== 0) {
|
||||
return null;
|
||||
}
|
||||
if (!ma && ma !== 0) 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 {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(60 + ((close - ma) / ma) * 500, 100),
|
||||
strength: 80,
|
||||
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 {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(60 + ((ma - close) / ma) * 500, 100),
|
||||
strength: 80,
|
||||
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
|
||||
|
||||
@ -38,44 +38,32 @@ function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValue
|
||||
const overbought = indicator.params?.overbought || 70;
|
||||
const oversold = indicator.params?.oversold || 30;
|
||||
|
||||
if (!rsi || rsi === null) {
|
||||
if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === 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 { type: signalType, strength, value: rsi, reasoning };
|
||||
}
|
||||
|
||||
// RSI Indicator class
|
||||
export class RSIIndicator extends BaseIndicator {
|
||||
constructor(config) {
|
||||
|
||||
@ -32,33 +32,38 @@ class BaseIndicator {
|
||||
}
|
||||
|
||||
// Signal calculation for Stochastic
|
||||
function calculateStochSignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const k = values?.k;
|
||||
const d = values?.d;
|
||||
const prevK = prevValues?.k;
|
||||
const prevD = prevValues?.d;
|
||||
const overbought = indicator.params?.overbought || 80;
|
||||
const oversold = indicator.params?.oversold || 20;
|
||||
|
||||
if (!k || !d) {
|
||||
if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) {
|
||||
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 {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(50 + (oversold - k) * 2, 100),
|
||||
strength: 80,
|
||||
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 {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(50 + (k - overbought) * 2, 100),
|
||||
strength: 80,
|
||||
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
|
||||
|
||||
@ -21,10 +21,21 @@ 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();
|
||||
this.initChart();
|
||||
@ -588,7 +599,14 @@ 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