feat: enhance strategy simulation with Inverse Perpetual support, effective leverage caps, and advanced charting
This commit is contained in:
@ -697,3 +697,30 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group {
|
||||||
|
display: flex;
|
||||||
|
background: var(--tv-hover);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--tv-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn.active {
|
||||||
|
background: var(--tv-border);
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toggle-group .toggle-btn:hover:not(.active) {
|
||||||
|
color: var(--tv-text);
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ constructor() {
|
|||||||
this.summarySignal = null;
|
this.summarySignal = null;
|
||||||
this.lastCandleTimestamp = null;
|
this.lastCandleTimestamp = null;
|
||||||
this.simulationMarkers = [];
|
this.simulationMarkers = [];
|
||||||
|
this.avgPriceSeries = null;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@ -36,6 +37,18 @@ constructor() {
|
|||||||
this.updateSignalMarkers();
|
this.updateSignalMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAvgPriceData(data) {
|
||||||
|
if (this.avgPriceSeries) {
|
||||||
|
this.avgPriceSeries.setData(data || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAvgPriceData() {
|
||||||
|
if (this.avgPriceSeries) {
|
||||||
|
this.avgPriceSeries.setData([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.createTimeframeButtons();
|
this.createTimeframeButtons();
|
||||||
this.initChart();
|
this.initChart();
|
||||||
@ -129,6 +142,16 @@ constructor() {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
|
color: '#00bcd4',
|
||||||
|
lineWidth: 1,
|
||||||
|
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||||
|
lastValueVisible: true,
|
||||||
|
priceLineVisible: false,
|
||||||
|
crosshairMarkerVisible: false,
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
this.currentPriceLine = this.candleSeries.createPriceLine({
|
this.currentPriceLine = this.candleSeries.createPriceLine({
|
||||||
price: 0,
|
price: 0,
|
||||||
color: '#26a69a',
|
color: '#26a69a',
|
||||||
|
|||||||
@ -4,6 +4,43 @@ let activeIndicators = [];
|
|||||||
let simulationResults = null;
|
let simulationResults = null;
|
||||||
let equitySeries = null;
|
let equitySeries = null;
|
||||||
let equityChart = null;
|
let equityChart = null;
|
||||||
|
let posSeries = null;
|
||||||
|
let posSizeChart = null;
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ping_pong_settings';
|
||||||
|
|
||||||
|
function getSavedSettings() {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!saved) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
const settings = {
|
||||||
|
startDate: document.getElementById('simStartDate').value,
|
||||||
|
contractType: document.getElementById('simContractType').value,
|
||||||
|
direction: document.getElementById('simDirection').value,
|
||||||
|
capital: document.getElementById('simCapital').value,
|
||||||
|
exchangeLeverage: document.getElementById('simExchangeLeverage').value,
|
||||||
|
maxEffectiveLeverage: document.getElementById('simMaxEffectiveLeverage').value,
|
||||||
|
posSize: document.getElementById('simPosSize').value,
|
||||||
|
tp: document.getElementById('simTP').value
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
|
||||||
|
const btn = document.getElementById('saveSimSettings');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = 'Saved!';
|
||||||
|
btn.style.color = '#26a69a';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.style.color = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
export function initStrategyPanel() {
|
export function initStrategyPanel() {
|
||||||
window.renderStrategyPanel = renderStrategyPanel;
|
window.renderStrategyPanel = renderStrategyPanel;
|
||||||
@ -30,6 +67,7 @@ export function renderStrategyPanel() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
activeIndicators = window.getActiveIndicators?.() || [];
|
activeIndicators = window.getActiveIndicators?.() || [];
|
||||||
|
const saved = getSavedSettings();
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
@ -39,39 +77,55 @@ export function renderStrategyPanel() {
|
|||||||
<div class="sidebar-section-content">
|
<div class="sidebar-section-content">
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Start Date & Time</label>
|
<label>Start Date & Time</label>
|
||||||
<input type="datetime-local" id="simStartDate" class="sim-input" value="2026-01-01T00:00">
|
<input type="datetime-local" id="simStartDate" class="sim-input" value="${saved?.startDate || '2026-01-01T00:00'}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Contract Type</label>
|
||||||
|
<select id="simContractType" class="sim-input">
|
||||||
|
<option value="linear" ${saved?.contractType === 'linear' ? 'selected' : ''}>Linear (USDT-Margined)</option>
|
||||||
|
<option value="inverse" ${saved?.contractType === 'inverse' ? 'selected' : ''}>Inverse (Coin-Margined)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Direction</label>
|
<label>Direction</label>
|
||||||
<select id="simDirection" class="sim-input">
|
<select id="simDirection" class="sim-input">
|
||||||
<option value="long" selected>Long</option>
|
<option value="long" ${saved?.direction === 'long' ? 'selected' : ''}>Long</option>
|
||||||
<option value="short">Short</option>
|
<option value="short" ${saved?.direction === 'short' ? 'selected' : ''}>Short</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Initial Capital ($)</label>
|
<label>Initial Capital ($)</label>
|
||||||
<input type="number" id="simCapital" class="sim-input" value="10000" min="1">
|
<input type="number" id="simCapital" class="sim-input" value="${saved?.capital || '10000'}" min="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Leverage</label>
|
<label>Exchange Leverage (Ping Size Multiplier)</label>
|
||||||
<input type="number" id="simLeverage" class="sim-input" value="1" min="1" max="100">
|
<input type="number" id="simExchangeLeverage" class="sim-input" value="${saved?.exchangeLeverage || '1'}" min="1" max="100">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Position Size ($)</label>
|
<label>Max Effective Leverage (Total Account Cap)</label>
|
||||||
<input type="number" id="simPosSize" class="sim-input" value="10" min="1">
|
<input type="number" id="simMaxEffectiveLeverage" class="sim-input" value="${saved?.maxEffectiveLeverage || '5'}" min="1" max="100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sim-input-group">
|
||||||
|
<label>Position Size ($ Margin per Ping)</label>
|
||||||
|
<input type="number" id="simPosSize" class="sim-input" value="${saved?.posSize || '10'}" min="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Take Profit (%)</label>
|
<label>Take Profit (%)</label>
|
||||||
<input type="number" id="simTP" class="sim-input" value="15" step="0.1" min="0.1">
|
<input type="number" id="simTP" class="sim-input" value="${saved?.tp || '15'}" step="0.1" min="0.1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-input-group">
|
<div class="sim-input-group">
|
||||||
<label>Open Signal Indicators</label>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||||
|
<label style="margin-bottom: 0;">Open Signal Indicators</label>
|
||||||
|
<button class="action-btn-text" id="saveSimSettings" style="font-size: 10px; color: #00bcd4; background: none; border: none; cursor: pointer; padding: 0;">Save Defaults</button>
|
||||||
|
</div>
|
||||||
<div class="indicator-checklist" id="openSignalsList">
|
<div class="indicator-checklist" id="openSignalsList">
|
||||||
${renderIndicatorChecklist('open')}
|
${renderIndicatorChecklist('open')}
|
||||||
</div>
|
</div>
|
||||||
@ -94,6 +148,7 @@ export function renderStrategyPanel() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
document.getElementById('runSimulationBtn').addEventListener('click', runSimulation);
|
document.getElementById('runSimulationBtn').addEventListener('click', runSimulation);
|
||||||
|
document.getElementById('saveSimSettings').addEventListener('click', saveSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderIndicatorChecklist(prefix) {
|
function renderIndicatorChecklist(prefix) {
|
||||||
@ -117,9 +172,11 @@ async function runSimulation() {
|
|||||||
try {
|
try {
|
||||||
const config = {
|
const config = {
|
||||||
startDate: new Date(document.getElementById('simStartDate').value).getTime() / 1000,
|
startDate: new Date(document.getElementById('simStartDate').value).getTime() / 1000,
|
||||||
|
contractType: document.getElementById('simContractType').value,
|
||||||
direction: document.getElementById('simDirection').value,
|
direction: document.getElementById('simDirection').value,
|
||||||
capital: parseFloat(document.getElementById('simCapital').value),
|
capital: parseFloat(document.getElementById('simCapital').value),
|
||||||
leverage: parseFloat(document.getElementById('simLeverage').value),
|
exchangeLeverage: parseFloat(document.getElementById('simExchangeLeverage').value),
|
||||||
|
maxEffectiveLeverage: parseFloat(document.getElementById('simMaxEffectiveLeverage').value),
|
||||||
posSize: parseFloat(document.getElementById('simPosSize').value),
|
posSize: parseFloat(document.getElementById('simPosSize').value),
|
||||||
tp: parseFloat(document.getElementById('simTP').value) / 100,
|
tp: parseFloat(document.getElementById('simTP').value) / 100,
|
||||||
openIndicators: Array.from(document.querySelectorAll('.sim-open-check:checked')).map(el => el.dataset.id),
|
openIndicators: Array.from(document.querySelectorAll('.sim-open-check:checked')).map(el => el.dataset.id),
|
||||||
@ -144,55 +201,48 @@ async function runSimulation() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate all indicator values and signals for the sim period
|
// Calculate indicator signals
|
||||||
const indicatorSignals = {}; // { indicatorId: [signals per candle] }
|
const indicatorSignals = {};
|
||||||
|
|
||||||
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 signalFunc = getSignalFunction(ind.type);
|
const signalFunc = getSignalFunction(ind.type);
|
||||||
const results = ind.cachedResults; // Use already calculated results from chart
|
const results = ind.cachedResults;
|
||||||
|
|
||||||
if (results && signalFunc) {
|
if (results && signalFunc) {
|
||||||
// Map results to simCandles indices
|
indicatorSignals[indId] = 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 values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] };
|
||||||
const res = results[idx];
|
const prevValues = typeof results[idx-1] === 'object' && results[idx-1] !== null ? results[idx-1] : { ma: results[idx-1] };
|
||||||
const prevRes = results[idx-1];
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
} else {
|
|
||||||
console.warn(`[Simulation] Missing cached data or signal function for ${indId}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulation loop
|
// Simulation loop
|
||||||
let balance = config.capital;
|
const startPrice = simCandles[0].open;
|
||||||
let equity = [{ time: simCandles[0].time, value: balance }];
|
let balanceBtc = config.contractType === 'inverse' ? config.capital / startPrice : 0;
|
||||||
let totalQty = 0; // Total position quantity in BTC
|
let balanceUsd = config.contractType === 'linear' ? config.capital : 0;
|
||||||
let avgPrice = 0;
|
|
||||||
let trades = []; // { type, recordType, time, entryPrice, exitPrice, pnl, reason, currentUsd, currentQty }
|
|
||||||
|
|
||||||
const PARTIAL_EXIT_PCT = 0.15; // 15% partial exit
|
let equityData = { usd: [], btc: [] };
|
||||||
|
let totalQty = 0; // Linear: BTC, Inverse: USD Contracts
|
||||||
|
let avgPrice = 0;
|
||||||
|
let avgPriceData = [];
|
||||||
|
let posSizeData = { btc: [], usd: [] };
|
||||||
|
let trades = [];
|
||||||
|
|
||||||
|
const PARTIAL_EXIT_PCT = 0.15;
|
||||||
|
const MIN_POSITION_VALUE_USD = 15;
|
||||||
|
|
||||||
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;
|
let actionTakenInThisCandle = false;
|
||||||
|
|
||||||
// 1. Check TP for total position
|
// 1. Check TP
|
||||||
if (totalQty > 0) {
|
if (totalQty > 0) {
|
||||||
let isTP = false;
|
let isTP = false;
|
||||||
let exitPrice = price;
|
let exitPrice = price;
|
||||||
|
|
||||||
if (config.direction === 'long') {
|
if (config.direction === 'long') {
|
||||||
if (candle.high >= avgPrice * (1 + config.tp)) {
|
if (candle.high >= avgPrice * (1 + config.tp)) {
|
||||||
isTP = true;
|
isTP = true;
|
||||||
@ -206,30 +256,40 @@ async function runSimulation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTP) {
|
if (isTP) {
|
||||||
const qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
let qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
||||||
const pnl = config.direction === 'long'
|
let remainingQty = totalQty - qtyToClose;
|
||||||
? (exitPrice - avgPrice) * qtyToClose
|
let remainingValueUsd = config.contractType === 'linear' ? remainingQty * exitPrice : remainingQty;
|
||||||
: (avgPrice - exitPrice) * qtyToClose;
|
let reason = 'TP (Partial)';
|
||||||
|
|
||||||
|
// Minimum size check
|
||||||
|
if (remainingValueUsd < MIN_POSITION_VALUE_USD) {
|
||||||
|
qtyToClose = totalQty;
|
||||||
|
reason = 'TP (Full - Min Size)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnl;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnl = config.direction === 'long' ? (exitPrice - avgPrice) * qtyToClose : (avgPrice - exitPrice) * qtyToClose;
|
||||||
|
balanceUsd += pnl;
|
||||||
|
} else {
|
||||||
|
pnl = config.direction === 'long'
|
||||||
|
? qtyToClose * (1/avgPrice - 1/exitPrice)
|
||||||
|
: qtyToClose * (1/exitPrice - 1/avgPrice);
|
||||||
|
balanceBtc += pnl;
|
||||||
|
}
|
||||||
|
|
||||||
balance += pnl;
|
|
||||||
totalQty -= qtyToClose;
|
totalQty -= qtyToClose;
|
||||||
|
|
||||||
trades.push({
|
trades.push({
|
||||||
type: config.direction,
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
recordType: 'exit',
|
entryPrice: avgPrice, exitPrice: exitPrice, pnl: pnl, reason: reason,
|
||||||
time: candle.time,
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
entryPrice: avgPrice,
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
exitPrice: exitPrice,
|
|
||||||
pnl: pnl,
|
|
||||||
reason: 'TP (Partial)',
|
|
||||||
currentUsd: totalQty * price,
|
|
||||||
currentQty: totalQty
|
|
||||||
});
|
});
|
||||||
actionTakenInThisCandle = true;
|
actionTakenInThisCandle = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Close Signals (Partial Exit) - Only if TP didn't trigger
|
// 2. Check Close Signals
|
||||||
if (!actionTakenInThisCandle && totalQty > 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];
|
||||||
@ -238,30 +298,52 @@ async function runSimulation() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (hasCloseSignal) {
|
if (hasCloseSignal) {
|
||||||
const qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
let qtyToClose = totalQty * PARTIAL_EXIT_PCT;
|
||||||
const pnl = config.direction === 'long'
|
let remainingQty = totalQty - qtyToClose;
|
||||||
? (price - avgPrice) * qtyToClose
|
let remainingValueUsd = config.contractType === 'linear' ? remainingQty * price : remainingQty;
|
||||||
: (avgPrice - price) * qtyToClose;
|
let reason = 'Signal (Partial)';
|
||||||
|
|
||||||
|
// Minimum size check
|
||||||
|
if (remainingValueUsd < MIN_POSITION_VALUE_USD) {
|
||||||
|
qtyToClose = totalQty;
|
||||||
|
reason = 'Signal (Full - Min Size)';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnl;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
pnl = config.direction === 'long' ? (price - avgPrice) * qtyToClose : (avgPrice - price) * qtyToClose;
|
||||||
|
balanceUsd += pnl;
|
||||||
|
} else {
|
||||||
|
pnl = config.direction === 'long'
|
||||||
|
? qtyToClose * (1/avgPrice - 1/price)
|
||||||
|
: qtyToClose * (1/price - 1/avgPrice);
|
||||||
|
balanceBtc += pnl;
|
||||||
|
}
|
||||||
|
|
||||||
balance += pnl;
|
|
||||||
totalQty -= qtyToClose;
|
totalQty -= qtyToClose;
|
||||||
|
|
||||||
trades.push({
|
trades.push({
|
||||||
type: config.direction,
|
type: config.direction, recordType: 'exit', time: candle.time,
|
||||||
recordType: 'exit',
|
entryPrice: avgPrice, exitPrice: price, pnl: pnl, reason: reason,
|
||||||
time: candle.time,
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
entryPrice: avgPrice,
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
exitPrice: price,
|
|
||||||
pnl: pnl,
|
|
||||||
reason: 'Signal (Partial)',
|
|
||||||
currentUsd: totalQty * price,
|
|
||||||
currentQty: totalQty
|
|
||||||
});
|
});
|
||||||
actionTakenInThisCandle = true;
|
actionTakenInThisCandle = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check Open Signals (Pyramiding) - Only if no exit happened in this candle
|
// Calculate Current Equity for Margin Check
|
||||||
|
let currentEquityBtc, currentEquityUsd;
|
||||||
|
if (config.contractType === 'linear') {
|
||||||
|
const upnlUsd = totalQty > 0 ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty : 0;
|
||||||
|
currentEquityUsd = balanceUsd + upnlUsd;
|
||||||
|
currentEquityBtc = currentEquityUsd / price;
|
||||||
|
} else {
|
||||||
|
const upnlBtc = totalQty > 0 ? (config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice)) : 0;
|
||||||
|
currentEquityBtc = balanceBtc + upnlBtc;
|
||||||
|
currentEquityUsd = currentEquityBtc * price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Open Signals
|
||||||
if (!actionTakenInThisCandle) {
|
if (!actionTakenInThisCandle) {
|
||||||
const hasOpenSignal = config.openIndicators.some(id => {
|
const hasOpenSignal = config.openIndicators.some(id => {
|
||||||
const sig = indicatorSignals[id][i];
|
const sig = indicatorSignals[id][i];
|
||||||
@ -270,54 +352,88 @@ async function runSimulation() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (hasOpenSignal) {
|
if (hasOpenSignal) {
|
||||||
const entryUsd = config.posSize * config.leverage;
|
const entryValUsd = config.posSize * config.exchangeLeverage;
|
||||||
const entryQty = entryUsd / price;
|
const currentNotionalBtc = config.contractType === 'linear' ? totalQty : totalQty / price;
|
||||||
|
const entryNotionalBtc = entryValUsd / price;
|
||||||
|
|
||||||
const newQty = totalQty + entryQty;
|
const projectedEffectiveLeverage = (currentNotionalBtc + entryNotionalBtc) / Math.max(currentEquityBtc, 0.0000001);
|
||||||
avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / newQty;
|
|
||||||
totalQty = newQty;
|
|
||||||
|
|
||||||
trades.push({
|
if (projectedEffectiveLeverage <= config.maxEffectiveLeverage) {
|
||||||
type: config.direction,
|
if (config.contractType === 'linear') {
|
||||||
recordType: 'entry',
|
const entryQty = entryValUsd / price;
|
||||||
time: candle.time,
|
avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / (totalQty + entryQty);
|
||||||
entryPrice: price,
|
totalQty += entryQty;
|
||||||
reason: 'Entry',
|
} else {
|
||||||
currentUsd: totalQty * price,
|
// Inverse: totalQty is USD contracts
|
||||||
currentQty: totalQty
|
avgPrice = (totalQty + entryValUsd) / ((totalQty / avgPrice || 0) + (entryValUsd / price));
|
||||||
});
|
totalQty += entryValUsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
trades.push({
|
||||||
|
type: config.direction, recordType: 'entry', time: candle.time,
|
||||||
|
entryPrice: price, reason: 'Entry',
|
||||||
|
currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty,
|
||||||
|
currentQty: config.contractType === 'linear' ? totalQty : totalQty / price
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate current equity (realized balance + unrealized PnL)
|
// Final Equity Recording
|
||||||
const unrealizedPnl = totalQty > 0
|
let finalEquityBtc, finalEquityUsd;
|
||||||
? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty
|
if (config.contractType === 'linear') {
|
||||||
: 0;
|
const upnl = totalQty > 0 ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty : 0;
|
||||||
|
finalEquityUsd = balanceUsd + upnl;
|
||||||
|
finalEquityBtc = finalEquityUsd / price;
|
||||||
|
} else {
|
||||||
|
const upnl = totalQty > 0 ? (config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice)) : 0;
|
||||||
|
finalEquityBtc = balanceBtc + upnl;
|
||||||
|
finalEquityUsd = finalEquityBtc * price;
|
||||||
|
}
|
||||||
|
|
||||||
equity.push({ time: candle.time, value: balance + unrealizedPnl });
|
equityData.usd.push({ time: candle.time, value: finalEquityUsd });
|
||||||
|
equityData.btc.push({ time: candle.time, value: finalEquityBtc });
|
||||||
|
|
||||||
|
if (totalQty > 0.000001) avgPriceData.push({ time: candle.time, value: avgPrice });
|
||||||
|
posSizeData.btc.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty : totalQty / price });
|
||||||
|
posSizeData.usd.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty * price : totalQty });
|
||||||
}
|
}
|
||||||
|
|
||||||
displayResults(trades, equity, config);
|
displayResults(trades, equityData, config, simCandles[simCandles.length-1].close, avgPriceData, posSizeData);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Simulation] Error:', error);
|
console.error('[Simulation] Error:', error);
|
||||||
alert('Simulation failed. See console for details.');
|
alert('Simulation failed.');
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = 'Run Simulation';
|
btn.textContent = 'Run Simulation';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayResults(trades, equity, config) {
|
function displayResults(trades, equityData, config, endPrice, avgPriceData, posSizeData) {
|
||||||
const resultsDiv = document.getElementById('simulationResults');
|
const resultsDiv = document.getElementById('simulationResults');
|
||||||
resultsDiv.style.display = 'block';
|
resultsDiv.style.display = 'block';
|
||||||
|
|
||||||
const totalTrades = trades.length;
|
// Update main chart with avg price
|
||||||
const profitableTrades = trades.filter(t => t.pnl > 0).length;
|
if (window.dashboard) {
|
||||||
const winRate = totalTrades > 0 ? (profitableTrades / totalTrades * 100).toFixed(1) : 0;
|
window.dashboard.setAvgPriceData(avgPriceData);
|
||||||
const totalPnl = trades.reduce((sum, t) => sum + t.pnl, 0);
|
}
|
||||||
const finalBalance = config.capital + totalPnl;
|
|
||||||
const roi = (totalPnl / config.capital * 100).toFixed(2);
|
const entryTrades = trades.filter(t => t.recordType === 'entry').length;
|
||||||
|
const exitTrades = trades.filter(t => t.recordType === 'exit').length;
|
||||||
|
const profitableTrades = trades.filter(t => t.recordType === 'exit' && t.pnl > 0).length;
|
||||||
|
const winRate = exitTrades > 0 ? (profitableTrades / exitTrades * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
const startPrice = equityData.usd[0].value / equityData.btc[0].value;
|
||||||
|
const startBtc = config.capital / startPrice;
|
||||||
|
|
||||||
|
const finalUsd = equityData.usd[equityData.usd.length - 1].value;
|
||||||
|
const finalBtc = finalUsd / endPrice;
|
||||||
|
|
||||||
|
const totalPnlUsd = finalUsd - config.capital;
|
||||||
|
const roi = (totalPnlUsd / config.capital * 100).toFixed(2);
|
||||||
|
|
||||||
|
const roiBtc = ((finalBtc - startBtc) / startBtc * 100).toFixed(2);
|
||||||
|
|
||||||
resultsDiv.innerHTML = `
|
resultsDiv.innerHTML = `
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
@ -325,30 +441,46 @@ function displayResults(trades, equity, config) {
|
|||||||
<div class="sidebar-section-content">
|
<div class="sidebar-section-content">
|
||||||
<div class="results-summary">
|
<div class="results-summary">
|
||||||
<div class="result-stat">
|
<div class="result-stat">
|
||||||
<div class="result-stat-value ${totalPnl >= 0 ? 'positive' : 'negative'}">${roi}%</div>
|
<div class="result-stat-value ${totalPnlUsd >= 0 ? 'positive' : 'negative'}">${roi}%</div>
|
||||||
<div class="result-stat-label">ROI</div>
|
<div class="result-stat-label">ROI (USD)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-stat">
|
<div class="result-stat">
|
||||||
<div class="result-stat-value">${winRate}%</div>
|
<div class="result-stat-value ${parseFloat(roiBtc) >= 0 ? 'positive' : 'negative'}">${roiBtc}%</div>
|
||||||
<div class="result-stat-label">Win Rate</div>
|
<div class="result-stat-label">ROI (BTC)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sim-stat-row">
|
<div class="sim-stat-row">
|
||||||
<span>Total Trades</span>
|
<span>Starting Balance</span>
|
||||||
<span class="sim-value">${totalTrades}</span>
|
<span class="sim-value">$${config.capital.toFixed(0)} / ${startBtc.toFixed(4)} BTC</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>
|
||||||
<div class="sim-stat-row">
|
<div class="sim-stat-row">
|
||||||
<span>Final Balance</span>
|
<span>Final Balance</span>
|
||||||
<span class="sim-value">$${finalBalance.toFixed(2)}</span>
|
<span class="sim-value">$${finalUsd.toFixed(2)} / ${finalBtc.toFixed(4)} BTC</span>
|
||||||
|
</div>
|
||||||
|
<div class="sim-stat-row">
|
||||||
|
<span>Trades (Entry / Exit)</span>
|
||||||
|
<span class="sim-value">${entryTrades} / ${exitTrades}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
|
||||||
|
<span style="font-size: 11px; color: var(--tv-text-secondary);">Equity Chart</span>
|
||||||
|
<div class="chart-toggle-group">
|
||||||
|
<button class="toggle-btn active" data-unit="usd">USD</button>
|
||||||
|
<button class="toggle-btn" data-unit="btc">BTC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="equity-chart-container" id="equityChart"></div>
|
<div class="equity-chart-container" id="equityChart"></div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 12px;">
|
||||||
|
<span style="font-size: 11px; color: var(--tv-text-secondary);" id="posSizeLabel">Position Size (BTC)</span>
|
||||||
|
<div class="chart-toggle-group">
|
||||||
|
<button class="toggle-btn active" data-unit="usd">USD</button>
|
||||||
|
<button class="toggle-btn" data-unit="btc">BTC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="equity-chart-container" id="posSizeChart"></div>
|
||||||
|
|
||||||
<div class="results-actions">
|
<div class="results-actions">
|
||||||
<button class="action-btn secondary" id="toggleTradeMarkers">Show Markers</button>
|
<button class="action-btn secondary" id="toggleTradeMarkers">Show Markers</button>
|
||||||
<button class="action-btn secondary" id="clearSim">Clear</button>
|
<button class="action-btn secondary" id="clearSim">Clear</button>
|
||||||
@ -357,30 +489,86 @@ function displayResults(trades, equity, config) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Create Equity Chart
|
// Create Charts
|
||||||
setTimeout(() => {
|
const initCharts = () => {
|
||||||
const chartContainer = document.getElementById('equityChart');
|
// Equity Chart
|
||||||
if (!chartContainer) return;
|
const equityContainer = document.getElementById('equityChart');
|
||||||
|
if (equityContainer) {
|
||||||
|
equityContainer.innerHTML = '';
|
||||||
|
equityChart = LightweightCharts.createChart(equityContainer, {
|
||||||
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||||
|
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||||
|
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||||
|
timeScale: { borderColor: '#2a2e39', visible: false },
|
||||||
|
handleScroll: false,
|
||||||
|
handleScale: false
|
||||||
|
});
|
||||||
|
|
||||||
equityChart = LightweightCharts.createChart(chartContainer, {
|
equitySeries = equityChart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
lineColor: totalPnlUsd >= 0 ? '#26a69a' : '#ef5350',
|
||||||
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
topColor: totalPnlUsd >= 0 ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)',
|
||||||
rightPriceScale: { borderColor: '#2a2e39' },
|
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
timeScale: { borderColor: '#2a2e39', visible: false },
|
lineWidth: 2,
|
||||||
handleScroll: false,
|
});
|
||||||
handleScale: false
|
|
||||||
|
equitySeries.setData(equityData['usd']);
|
||||||
|
equityChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pos Size Chart
|
||||||
|
const posSizeContainer = document.getElementById('posSizeChart');
|
||||||
|
if (posSizeContainer) {
|
||||||
|
posSizeContainer.innerHTML = '';
|
||||||
|
posSizeChart = LightweightCharts.createChart(posSizeContainer, {
|
||||||
|
layout: { background: { color: '#131722' }, textColor: '#d1d4dc' },
|
||||||
|
grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } },
|
||||||
|
rightPriceScale: { borderColor: '#2a2e39', autoScale: true },
|
||||||
|
timeScale: { borderColor: '#2a2e39', visible: false },
|
||||||
|
handleScroll: false,
|
||||||
|
handleScale: false
|
||||||
|
});
|
||||||
|
|
||||||
|
posSeries = posSizeChart.addSeries(LightweightCharts.AreaSeries, {
|
||||||
|
lineColor: '#00bcd4',
|
||||||
|
topColor: 'rgba(0, 188, 212, 0.4)',
|
||||||
|
bottomColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
posSeries.setData(posSizeData['usd']);
|
||||||
|
posSizeChart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const label = document.getElementById('posSizeLabel');
|
||||||
|
if (label) label.textContent = 'Position Size (USD)';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(initCharts, 100);
|
||||||
|
|
||||||
|
// Toggle Logic
|
||||||
|
resultsDiv.querySelectorAll('.toggle-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const unit = btn.dataset.unit;
|
||||||
|
|
||||||
|
// Sync all toggle button groups
|
||||||
|
resultsDiv.querySelectorAll(`.toggle-btn`).forEach(b => {
|
||||||
|
if (b.dataset.unit === unit) b.classList.add('active');
|
||||||
|
else b.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (equitySeries) {
|
||||||
|
equitySeries.setData(equityData[unit]);
|
||||||
|
equityChart.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
if (posSeries) {
|
||||||
|
posSeries.setData(posSizeData[unit]);
|
||||||
|
posSizeChart.timeScale().fitContent();
|
||||||
|
|
||||||
|
const label = document.getElementById('posSizeLabel');
|
||||||
|
if (label) label.textContent = `Position Size (${unit.toUpperCase()})`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
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', () => {
|
document.getElementById('toggleTradeMarkers').addEventListener('click', () => {
|
||||||
toggleSimulationMarkers(trades);
|
toggleSimulationMarkers(trades);
|
||||||
@ -389,6 +577,9 @@ function displayResults(trades, equity, config) {
|
|||||||
document.getElementById('clearSim').addEventListener('click', () => {
|
document.getElementById('clearSim').addEventListener('click', () => {
|
||||||
resultsDiv.style.display = 'none';
|
resultsDiv.style.display = 'none';
|
||||||
clearSimulationMarkers();
|
clearSimulationMarkers();
|
||||||
|
if (window.dashboard) {
|
||||||
|
window.dashboard.clearAvgPriceData();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user