chore: add AGENTS.md with build, lint, test commands and style guidelines
This commit is contained in:
370
js/ui/strategy-panel.js
Normal file
370
js/ui/strategy-panel.js
Normal file
@ -0,0 +1,370 @@
|
||||
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();
|
||||
};
|
||||
Reference in New Issue
Block a user