Files
winterfail/js/ui/strategy-panel.js

370 lines
15 KiB
JavaScript

import { getStrategy, registerStrategy } from '../strategies/index.js';
import { PingPongStrategy } from '../strategies/ping-pong.js';
// Register available strategies
registerStrategy('ping_pong', PingPongStrategy);
let activeIndicators = [];
function formatDisplayDate(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
}
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?.() || [];
// For now, we only have Ping-Pong. Later we can add a strategy selector.
const currentStrategyId = 'ping_pong';
const strategy = getStrategy(currentStrategyId);
if (!strategy) {
container.innerHTML = `<div class="sidebar-section">Strategy not found.</div>`;
return;
}
container.innerHTML = `
<div class="sidebar-section">
<div class="sidebar-section-header">
<span>⚙️</span> ${strategy.name} Strategy
</div>
<div class="sidebar-section-content">
${strategy.renderUI(activeIndicators, formatDisplayDate)}
<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>
`;
// Attach strategy specific listeners (like disabling dropdowns when auto-detect is on)
if (strategy.attachListeners) {
strategy.attachListeners();
}
document.getElementById('runSimulationBtn').addEventListener('click', () => {
strategy.runSimulation(activeIndicators, displayResults);
});
}
// Keep the display logic here so all strategies can use the same rendering for results
let equitySeries = null;
let equityChart = null;
let posSeries = null;
let posSizeChart = null;
let tradeMarkers = [];
function displayResults(trades, equityData, config, endPrice, avgPriceData, posSizeData) {
const resultsDiv = document.getElementById('simulationResults');
resultsDiv.style.display = 'block';
if (window.dashboard) {
window.dashboard.setAvgPriceData(avgPriceData);
}
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 = `
<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 ${totalPnlUsd >= 0 ? 'positive' : 'negative'}">${roi}%</div>
<div class="result-stat-label">ROI (USD)</div>
</div>
<div class="result-stat">
<div class="result-stat-value ${parseFloat(roiBtc) >= 0 ? 'positive' : 'negative'}">${roiBtc}%</div>
<div class="result-stat-label">ROI (BTC)</div>
</div>
</div>
<div class="sim-stat-row">
<span>Starting Balance</span>
<span class="sim-value">$${config.capital.toFixed(0)} / ${startBtc.toFixed(4)} BTC</span>
</div>
<div class="sim-stat-row">
<span>Final Balance</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 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 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">
<button class="action-btn secondary" id="toggleTradeMarkers">Show Markers</button>
<button class="action-btn secondary" id="clearSim">Clear</button>
</div>
</div>
</div>
`;
// Create Charts
const initCharts = () => {
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: true,
timeVisible: true,
secondsVisible: false,
tickMarkFormatter: (time, tickMarkType, locale) => {
return window.TimezoneConfig ? window.TimezoneConfig.formatTickMark(time) : new Date(time * 1000).toLocaleDateString();
},
},
localization: {
timeFormatter: (timestamp) => {
return window.TimezoneConfig ? window.TimezoneConfig.formatDate(timestamp * 1000) : new Date(timestamp * 1000).toLocaleString();
},
},
handleScroll: true,
handleScale: true
});
equitySeries = equityChart.addSeries(LightweightCharts.AreaSeries, {
lineColor: totalPnlUsd >= 0 ? '#26a69a' : '#ef5350',
topColor: totalPnlUsd >= 0 ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)',
bottomColor: 'rgba(0, 0, 0, 0)',
lineWidth: 2,
});
equitySeries.setData(equityData['usd']);
equityChart.timeScale().fitContent();
}
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: true,
timeVisible: true,
secondsVisible: false,
tickMarkFormatter: (time, tickMarkType, locale) => {
return window.TimezoneConfig ? window.TimezoneConfig.formatTickMark(time) : new Date(time * 1000).toLocaleDateString();
},
},
localization: {
timeFormatter: (timestamp) => {
return window.TimezoneConfig ? window.TimezoneConfig.formatDate(timestamp * 1000) : new Date(timestamp * 1000).toLocaleString();
},
},
handleScroll: true,
handleScale: true
});
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)';
}
if (equityChart && posSizeChart) {
let isSyncing = false;
const syncCharts = (source, target) => {
if (isSyncing) return;
isSyncing = true;
const range = source.timeScale().getVisibleRange();
target.timeScale().setVisibleRange(range);
isSyncing = false;
};
equityChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(equityChart, posSizeChart));
posSizeChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(posSizeChart, equityChart));
}
const syncToMain = (param) => {
if (!param.time || !window.dashboard || !window.dashboard.chart) return;
const timeScale = window.dashboard.chart.timeScale();
const currentRange = timeScale.getVisibleRange();
if (!currentRange) return;
const width = currentRange.to - currentRange.from;
const halfWidth = width / 2;
timeScale.setVisibleRange({
from: param.time - halfWidth,
to: param.time + halfWidth
});
};
if (equityChart) equityChart.subscribeClick(syncToMain);
if (posSizeChart) posSizeChart.subscribeClick(syncToMain);
};
setTimeout(initCharts, 100);
resultsDiv.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const unit = btn.dataset.unit;
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()})`;
}
});
});
document.getElementById('toggleTradeMarkers').addEventListener('click', () => {
toggleSimulationMarkers(trades);
});
document.getElementById('clearSim').addEventListener('click', () => {
resultsDiv.style.display = 'none';
clearSimulationMarkers();
if (window.dashboard) {
window.dashboard.clearAvgPriceData();
}
});
}
function toggleSimulationMarkers(trades) {
if (tradeMarkers.length > 0) {
clearSimulationMarkers();
document.getElementById('toggleTradeMarkers').textContent = 'Show Markers';
return;
}
const markers = [];
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})`;
if (t.recordType === 'entry') {
markers.push({
time: t.time,
position: t.type === 'long' ? 'belowBar' : 'aboveBar',
color: t.type === 'long' ? '#2962ff' : '#9c27b0',
shape: t.type === 'long' ? 'arrowUp' : 'arrowDown',
text: `Entry ${t.type.toUpperCase()}${sizeStr}`
});
}
if (t.recordType === 'exit') {
markers.push({
time: t.time,
position: t.type === 'long' ? 'aboveBar' : 'belowBar',
color: t.pnl >= 0 ? '#26a69a' : '#ef5350',
shape: t.type === 'long' ? 'arrowDown' : 'arrowUp',
text: `Exit ${t.reason}${sizeStr}`
});
}
});
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();
};