Refactor strategy simulation to track BTC quantity and enhance chart markers

- Switch simulation to track BTC quantity instead of USD notional only
- Use current candle price for entry/exit quantity and PnL calculations
- Correctly weight average entry price by BTC quantity
- Update chart markers to display total position in format: ( / BTC size)
- Improve equity chart accuracy by accounting for BTC value fluctuations
This commit is contained in:
DiTus
2026-03-03 16:05:46 +01:00
parent 633a146e8e
commit 50c525a0bd

View File

@ -177,9 +177,9 @@ async function runSimulation() {
// 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 totalSize = 0; // Total position size in USD let totalQty = 0; // Total position quantity in BTC
let avgPrice = 0; let avgPrice = 0;
let trades = []; // { type, entryTime, exitTime, entryPrice, exitPrice, pnl, reason } let trades = []; // { type, recordType, time, entryPrice, exitPrice, pnl, reason, currentUsd, currentQty }
const PARTIAL_EXIT_PCT = 0.15; // 15% partial exit const PARTIAL_EXIT_PCT = 0.15; // 15% partial exit
@ -189,7 +189,7 @@ async function runSimulation() {
let actionTakenInThisCandle = false; let actionTakenInThisCandle = false;
// 1. Check TP for total position // 1. Check TP for total position
if (totalSize > 0) { if (totalQty > 0) {
let isTP = false; let isTP = false;
let exitPrice = price; let exitPrice = price;
@ -206,12 +206,14 @@ async function runSimulation() {
} }
if (isTP) { if (isTP) {
const amountToClose = totalSize * PARTIAL_EXIT_PCT; const qtyToClose = totalQty * PARTIAL_EXIT_PCT;
const pnl = config.direction === 'long' const pnl = config.direction === 'long'
? (exitPrice - avgPrice) / avgPrice * amountToClose * config.leverage ? (exitPrice - avgPrice) * qtyToClose
: (avgPrice - exitPrice) / avgPrice * amountToClose * config.leverage; : (avgPrice - exitPrice) * qtyToClose;
balance += pnl; balance += pnl;
totalQty -= qtyToClose;
trades.push({ trades.push({
type: config.direction, type: config.direction,
recordType: 'exit', recordType: 'exit',
@ -219,15 +221,16 @@ async function runSimulation() {
entryPrice: avgPrice, entryPrice: avgPrice,
exitPrice: exitPrice, exitPrice: exitPrice,
pnl: pnl, pnl: pnl,
reason: 'TP (Partial)' reason: 'TP (Partial)',
currentUsd: totalQty * price,
currentQty: totalQty
}); });
totalSize -= amountToClose;
actionTakenInThisCandle = true; actionTakenInThisCandle = true;
} }
} }
// 2. Check Close Signals (Partial Exit) - Only if TP didn't trigger // 2. Check Close Signals (Partial Exit) - Only if TP didn't trigger
if (!actionTakenInThisCandle && totalSize > 0 && config.closeIndicators.length > 0) { if (!actionTakenInThisCandle && totalQty > 0 && config.closeIndicators.length > 0) {
const hasCloseSignal = config.closeIndicators.some(id => { const hasCloseSignal = config.closeIndicators.some(id => {
const sig = indicatorSignals[id][i]; const sig = indicatorSignals[id][i];
if (!sig) return false; if (!sig) return false;
@ -235,12 +238,14 @@ async function runSimulation() {
}); });
if (hasCloseSignal) { if (hasCloseSignal) {
const amountToClose = totalSize * PARTIAL_EXIT_PCT; const qtyToClose = totalQty * PARTIAL_EXIT_PCT;
const pnl = config.direction === 'long' const pnl = config.direction === 'long'
? (price - avgPrice) / avgPrice * amountToClose * config.leverage ? (price - avgPrice) * qtyToClose
: (avgPrice - price) / avgPrice * amountToClose * config.leverage; : (avgPrice - price) * qtyToClose;
balance += pnl; balance += pnl;
totalQty -= qtyToClose;
trades.push({ trades.push({
type: config.direction, type: config.direction,
recordType: 'exit', recordType: 'exit',
@ -248,9 +253,10 @@ async function runSimulation() {
entryPrice: avgPrice, entryPrice: avgPrice,
exitPrice: price, exitPrice: price,
pnl: pnl, pnl: pnl,
reason: 'Signal (Partial)' reason: 'Signal (Partial)',
currentUsd: totalQty * price,
currentQty: totalQty
}); });
totalSize -= amountToClose;
actionTakenInThisCandle = true; actionTakenInThisCandle = true;
} }
} }
@ -263,24 +269,29 @@ async function runSimulation() {
return config.direction === 'long' ? sig.type === 'buy' : sig.type === 'sell'; return config.direction === 'long' ? sig.type === 'buy' : sig.type === 'sell';
}); });
if (hasOpenSignal && balance >= config.posSize) { if (hasOpenSignal) {
const newSize = totalSize + config.posSize; const entryUsd = config.posSize * config.leverage;
avgPrice = ((totalSize * avgPrice) + (config.posSize * price)) / newSize; const entryQty = entryUsd / price;
totalSize = newSize;
const newQty = totalQty + entryQty;
avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / newQty;
totalQty = newQty;
trades.push({ trades.push({
type: config.direction, type: config.direction,
recordType: 'entry', recordType: 'entry',
time: candle.time, time: candle.time,
entryPrice: price, entryPrice: price,
reason: 'Entry' reason: 'Entry',
currentUsd: totalQty * price,
currentQty: totalQty
}); });
} }
} }
// Calculate current equity (realized balance + unrealized PnL) // Calculate current equity (realized balance + unrealized PnL)
const unrealizedPnl = totalSize > 0 const unrealizedPnl = totalQty > 0
? (config.direction === 'long' ? (price - avgPrice) / avgPrice : (avgPrice - price) / avgPrice) * totalSize * config.leverage ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty
: 0; : 0;
equity.push({ time: candle.time, value: balance + unrealizedPnl }); equity.push({ time: candle.time, value: balance + unrealizedPnl });
@ -392,6 +403,10 @@ function toggleSimulationMarkers(trades) {
const markers = []; const markers = [];
trades.forEach(t => { trades.forEach(t => {
const usdVal = t.currentUsd !== undefined ? `$${t.currentUsd.toFixed(0)}` : '0';
const qtyVal = t.currentQty !== undefined ? `${t.currentQty.toFixed(4)} BTC` : '0';
const sizeStr = ` (${usdVal} / ${qtyVal})`;
// Entry marker // Entry marker
if (t.recordType === 'entry') { if (t.recordType === 'entry') {
markers.push({ markers.push({
@ -399,7 +414,7 @@ function toggleSimulationMarkers(trades) {
position: t.type === 'long' ? 'belowBar' : 'aboveBar', position: t.type === 'long' ? 'belowBar' : 'aboveBar',
color: t.type === 'long' ? '#2962ff' : '#9c27b0', color: t.type === 'long' ? '#2962ff' : '#9c27b0',
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown', shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
text: `Entry ${t.type.toUpperCase()}` text: `Entry ${t.type.toUpperCase()}${sizeStr}`
}); });
} }
@ -410,7 +425,7 @@ function toggleSimulationMarkers(trades) {
position: t.type === 'long' ? 'aboveBar' : 'belowBar', position: t.type === 'long' ? 'aboveBar' : 'belowBar',
color: t.pnl >= 0 ? '#26a69a' : '#ef5350', color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp', shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
text: `Exit ${t.reason} ($${t.pnl.toFixed(2)})` text: `Exit ${t.reason}${sizeStr}`
}); });
} }
}); });