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:
@ -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}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user