Refine strategy simulation with pyramiding, partial exits, and fixed marker logic
- Implement position pyramiding and 15% partial profit-taking - Add mutual exclusion between entry and exit in the same candle - Optimize simulation to use cached indicator results - Revert Hurst Bands signal logic to cross-down (dip entry) - Add safety filter for chart markers to prevent rendering errors
This commit is contained in:
@ -70,21 +70,21 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUY: Price crosses DOWN through lower Hurst Band
|
// BUY: Price crosses DOWN through lower Hurst Band (dip entry)
|
||||||
if (prevClose > prevLower && close <= lower) {
|
if (prevClose > prevLower && close <= lower) {
|
||||||
return {
|
return {
|
||||||
type: 'buy',
|
type: 'buy',
|
||||||
strength: 75,
|
strength: 80,
|
||||||
value: close,
|
value: close,
|
||||||
reasoning: `Price crossed DOWN through lower Hurst Band`
|
reasoning: `Price crossed DOWN through lower Hurst Band`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// SELL: Price crosses DOWN through upper Hurst Band (reversal from top)
|
// SELL: Price crosses DOWN through upper Hurst Band (reversal entry)
|
||||||
if (prevClose > prevUpper && close <= upper) {
|
if (prevClose > prevUpper && close <= upper) {
|
||||||
return {
|
return {
|
||||||
type: 'sell',
|
type: 'sell',
|
||||||
strength: 75,
|
strength: 80,
|
||||||
value: close,
|
value: close,
|
||||||
reasoning: `Price crossed DOWN through upper Hurst Band`
|
reasoning: `Price crossed DOWN through upper Hurst Band`
|
||||||
};
|
};
|
||||||
|
|||||||
@ -604,10 +604,14 @@ async loadSignals() {
|
|||||||
// Merge simulation markers if present
|
// Merge simulation markers if present
|
||||||
if (this.simulationMarkers && this.simulationMarkers.length > 0) {
|
if (this.simulationMarkers && this.simulationMarkers.length > 0) {
|
||||||
markers = [...markers, ...this.simulationMarkers];
|
markers = [...markers, ...this.simulationMarkers];
|
||||||
// Re-sort combined markers by time
|
|
||||||
markers.sort((a, b) => a.time - b.time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Filter out any markers with invalid timestamps before passing to chart
|
||||||
|
markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time));
|
||||||
|
|
||||||
|
// 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) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -149,128 +149,141 @@ async function runSimulation() {
|
|||||||
|
|
||||||
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
|
for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) {
|
||||||
const ind = activeIndicators.find(a => a.id === indId);
|
const ind = activeIndicators.find(a => a.id === indId);
|
||||||
const IndicatorClass = IndicatorRegistry[ind.type];
|
|
||||||
const signalFunc = getSignalFunction(ind.type);
|
const signalFunc = getSignalFunction(ind.type);
|
||||||
|
const results = ind.cachedResults; // Use already calculated results from chart
|
||||||
|
|
||||||
if (IndicatorClass && signalFunc) {
|
if (results && signalFunc) {
|
||||||
const instance = new IndicatorClass(ind);
|
// Map results to simCandles indices
|
||||||
const results = instance.calculate(candles); // Calculate on FULL history for correctness
|
|
||||||
|
|
||||||
// Map full history results to simCandles indices
|
|
||||||
const simSignals = simCandles.map(candle => {
|
const simSignals = simCandles.map(candle => {
|
||||||
const idx = candles.findIndex(c => c.time === candle.time);
|
const idx = candles.findIndex(c => c.time === candle.time);
|
||||||
if (idx < 1) return null;
|
if (idx < 1) return null;
|
||||||
|
|
||||||
const res = results[idx];
|
const res = results[idx];
|
||||||
const prevRes = results[idx-1];
|
const prevRes = results[idx-1];
|
||||||
const values = typeof res === 'object' ? res : { ma: res };
|
|
||||||
const prevValues = typeof prevRes === 'object' ? prevRes : { ma: prevRes };
|
// Standardize result format (some indicators return objects, some return single values)
|
||||||
|
const values = typeof res === 'object' && res !== null ? res : { ma: res };
|
||||||
|
const prevValues = typeof prevRes === 'object' && prevRes !== null ? prevRes : { ma: prevRes };
|
||||||
|
|
||||||
return signalFunc(ind, candles[idx], candles[idx-1], values, prevValues);
|
return signalFunc(ind, candles[idx], candles[idx-1], values, prevValues);
|
||||||
});
|
});
|
||||||
|
|
||||||
indicatorSignals[indId] = simSignals;
|
indicatorSignals[indId] = simSignals;
|
||||||
|
} else {
|
||||||
|
console.warn(`[Simulation] Missing cached data or signal function for ${indId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulation loop
|
// Simulation loop
|
||||||
let balance = config.capital;
|
let balance = config.capital;
|
||||||
let equity = [{ time: simCandles[0].time, value: balance }];
|
let equity = [{ time: simCandles[0].time, value: balance }];
|
||||||
let positions = []; // { entryPrice, size, type, entryTime }
|
let totalSize = 0; // Total position size in USD
|
||||||
let trades = []; // { type, entryTime, exitTime, entryPrice, exitPrice, pnl, result }
|
let avgPrice = 0;
|
||||||
|
let trades = []; // { type, entryTime, exitTime, entryPrice, exitPrice, pnl, reason }
|
||||||
|
|
||||||
|
const PARTIAL_EXIT_PCT = 0.15; // 15% partial exit
|
||||||
|
|
||||||
for (let i = 0; i < simCandles.length; i++) {
|
for (let i = 0; i < simCandles.length; i++) {
|
||||||
const candle = simCandles[i];
|
const candle = simCandles[i];
|
||||||
const price = candle.close;
|
const price = candle.close;
|
||||||
|
let actionTakenInThisCandle = false;
|
||||||
|
|
||||||
// 1. Check TP for existing positions
|
// 1. Check TP for total position
|
||||||
for (let j = positions.length - 1; j >= 0; j--) {
|
if (totalSize > 0) {
|
||||||
const pos = positions[j];
|
let isTP = false;
|
||||||
let isClosed = false;
|
|
||||||
let exitPrice = price;
|
let exitPrice = price;
|
||||||
let reason = '';
|
|
||||||
|
|
||||||
// TP Logic
|
if (config.direction === 'long') {
|
||||||
if (pos.type === 'long') {
|
if (candle.high >= avgPrice * (1 + config.tp)) {
|
||||||
if (candle.high >= pos.entryPrice * (1 + config.tp)) {
|
isTP = true;
|
||||||
isClosed = true;
|
exitPrice = avgPrice * (1 + config.tp);
|
||||||
exitPrice = pos.entryPrice * (1 + config.tp);
|
|
||||||
reason = 'TP';
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (candle.low <= pos.entryPrice * (1 - config.tp)) {
|
if (candle.low <= avgPrice * (1 - config.tp)) {
|
||||||
isClosed = true;
|
isTP = true;
|
||||||
exitPrice = pos.entryPrice * (1 - config.tp);
|
exitPrice = avgPrice * (1 - config.tp);
|
||||||
reason = 'TP';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close Signal Logic
|
if (isTP) {
|
||||||
if (!isClosed && config.closeIndicators.length > 0) {
|
const amountToClose = totalSize * PARTIAL_EXIT_PCT;
|
||||||
const hasCloseSignal = config.closeIndicators.some(id => {
|
const pnl = config.direction === 'long'
|
||||||
const sig = indicatorSignals[id][i];
|
? (exitPrice - avgPrice) / avgPrice * amountToClose * config.leverage
|
||||||
if (!sig) return false;
|
: (avgPrice - exitPrice) / avgPrice * amountToClose * config.leverage;
|
||||||
|
|
||||||
// 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;
|
balance += pnl;
|
||||||
trades.push({
|
trades.push({
|
||||||
type: pos.type,
|
type: config.direction,
|
||||||
entryTime: pos.entryTime,
|
recordType: 'exit',
|
||||||
exitTime: candle.time,
|
time: candle.time,
|
||||||
entryPrice: pos.entryPrice,
|
entryPrice: avgPrice,
|
||||||
exitPrice: exitPrice,
|
exitPrice: exitPrice,
|
||||||
pnl: pnl,
|
pnl: pnl,
|
||||||
reason: reason
|
reason: 'TP (Partial)'
|
||||||
});
|
});
|
||||||
positions.splice(j, 1);
|
totalSize -= amountToClose;
|
||||||
|
actionTakenInThisCandle = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Open Signals
|
// 2. Check Close Signals (Partial Exit) - Only if TP didn't trigger
|
||||||
const hasOpenSignal = config.openIndicators.some(id => {
|
if (!actionTakenInThisCandle && totalSize > 0 && config.closeIndicators.length > 0) {
|
||||||
const sig = indicatorSignals[id][i];
|
const hasCloseSignal = config.closeIndicators.some(id => {
|
||||||
if (!sig) return false;
|
const sig = indicatorSignals[id][i];
|
||||||
|
if (!sig) return false;
|
||||||
if (config.direction === 'long') {
|
return config.direction === 'long' ? sig.type === 'sell' : sig.type === 'buy';
|
||||||
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
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasCloseSignal) {
|
||||||
|
const amountToClose = totalSize * PARTIAL_EXIT_PCT;
|
||||||
|
const pnl = config.direction === 'long'
|
||||||
|
? (price - avgPrice) / avgPrice * amountToClose * config.leverage
|
||||||
|
: (avgPrice - price) / avgPrice * amountToClose * config.leverage;
|
||||||
|
|
||||||
|
balance += pnl;
|
||||||
|
trades.push({
|
||||||
|
type: config.direction,
|
||||||
|
recordType: 'exit',
|
||||||
|
time: candle.time,
|
||||||
|
entryPrice: avgPrice,
|
||||||
|
exitPrice: price,
|
||||||
|
pnl: pnl,
|
||||||
|
reason: 'Signal (Partial)'
|
||||||
|
});
|
||||||
|
totalSize -= amountToClose;
|
||||||
|
actionTakenInThisCandle = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
equity.push({ time: candle.time, value: balance });
|
// 3. Check Open Signals (Pyramiding) - Only if no exit happened in this candle
|
||||||
|
if (!actionTakenInThisCandle) {
|
||||||
|
const hasOpenSignal = config.openIndicators.some(id => {
|
||||||
|
const sig = indicatorSignals[id][i];
|
||||||
|
if (!sig) return false;
|
||||||
|
return config.direction === 'long' ? sig.type === 'buy' : sig.type === 'sell';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasOpenSignal && balance >= config.posSize) {
|
||||||
|
const newSize = totalSize + config.posSize;
|
||||||
|
avgPrice = ((totalSize * avgPrice) + (config.posSize * price)) / newSize;
|
||||||
|
totalSize = newSize;
|
||||||
|
|
||||||
|
trades.push({
|
||||||
|
type: config.direction,
|
||||||
|
recordType: 'entry',
|
||||||
|
time: candle.time,
|
||||||
|
entryPrice: price,
|
||||||
|
reason: 'Entry'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate current equity (realized balance + unrealized PnL)
|
||||||
|
const unrealizedPnl = totalSize > 0
|
||||||
|
? (config.direction === 'long' ? (price - avgPrice) / avgPrice : (avgPrice - price) / avgPrice) * totalSize * config.leverage
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
equity.push({ time: candle.time, value: balance + unrealizedPnl });
|
||||||
}
|
}
|
||||||
|
|
||||||
displayResults(trades, equity, config);
|
displayResults(trades, equity, config);
|
||||||
@ -380,22 +393,26 @@ function toggleSimulationMarkers(trades) {
|
|||||||
const markers = [];
|
const markers = [];
|
||||||
trades.forEach(t => {
|
trades.forEach(t => {
|
||||||
// Entry marker
|
// Entry marker
|
||||||
markers.push({
|
if (t.recordType === 'entry') {
|
||||||
time: t.entryTime,
|
markers.push({
|
||||||
position: t.type === 'long' ? 'belowBar' : 'aboveBar',
|
time: t.time,
|
||||||
color: t.type === 'long' ? '#2962ff' : '#9c27b0',
|
position: t.type === 'long' ? 'belowBar' : 'aboveBar',
|
||||||
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
|
color: t.type === 'long' ? '#2962ff' : '#9c27b0',
|
||||||
text: `Entry ${t.type.toUpperCase()}`
|
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
|
||||||
});
|
text: `Entry ${t.type.toUpperCase()}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Exit marker
|
// Exit marker
|
||||||
markers.push({
|
if (t.recordType === 'exit') {
|
||||||
time: t.exitTime,
|
markers.push({
|
||||||
position: t.type === 'long' ? 'aboveBar' : 'belowBar',
|
time: t.time,
|
||||||
color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
|
position: t.type === 'long' ? 'aboveBar' : 'belowBar',
|
||||||
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
|
color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
|
||||||
text: `Exit ${t.reason} ($${t.pnl.toFixed(2)})`
|
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
|
||||||
});
|
text: `Exit ${t.reason} ($${t.pnl.toFixed(2)})`
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort markers by time
|
// Sort markers by time
|
||||||
|
|||||||
Reference in New Issue
Block a user