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 {
|
.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;
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
// BUY when RSI crosses UP through oversold band (bottom band)
|
return {
|
||||||
// RSI was below oversold, now above oversold
|
type: SIGNAL_TYPES.BUY,
|
||||||
if (prevRsi !== undefined && prevRsi !== null && prevRsi < oversold && rsi >= oversold) {
|
strength: 75,
|
||||||
signalType = SIGNAL_TYPES.BUY;
|
value: rsi,
|
||||||
strength = Math.min(50 + (rsi - oversold) * 2, 100);
|
reasoning: `RSI crossed UP through oversold level (${oversold})`
|
||||||
reasoning = `RSI (${rsi.toFixed(2)}) crossed up through oversold level (${oversold})`;
|
};
|
||||||
}
|
}
|
||||||
// SELL when RSI crosses DOWN through overbought band (top band)
|
// SELL when RSI crosses DOWN through overbought level
|
||||||
// RSI was above overbought, now below overbought
|
else if (prevRsi > overbought && rsi <= overbought) {
|
||||||
else if (prevRsi !== undefined && prevRsi !== null && prevRsi > overbought && rsi <= overbought) {
|
return {
|
||||||
signalType = SIGNAL_TYPES.SELL;
|
type: SIGNAL_TYPES.SELL,
|
||||||
strength = Math.min(50 + (overbought - rsi) * 2, 100);
|
strength: 75,
|
||||||
reasoning = `RSI (${rsi.toFixed(2)}) crossed down through overbought level (${overbought})`;
|
value: rsi,
|
||||||
}
|
reasoning: `RSI 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 };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RSI Indicator class
|
// RSI Indicator class
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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