/**
* Time & Sales Tape Component
* Live scrolling trade prints with big trade highlighting
*/
class TimeAndSales {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = {
maxTrades: 100, // Max trades to keep in memory
displayTrades: 50, // Max trades to display
bigTradeThreshold: 20, // Contracts to highlight as big trade
autoScroll: true,
showAggressor: true,
colors: {
background: '#1c2128',
buy: '#3fb950',
sell: '#f85149',
neutral: '#8b949e',
bigTrade: '#d29922',
text: '#e6edf3',
textMuted: '#6e7681',
...options.colors
},
...options
};
this.trades = [];
this.cumulativeVolume = { buy: 0, sell: 0 };
this.paused = false;
this._init();
}
_init() {
this.container.innerHTML = `
`;
this.tbody = document.getElementById('tapeTbody');
this.bodyEl = document.getElementById('tapeBody');
this.buyVolEl = document.getElementById('tapeBuyVol');
this.sellVolEl = document.getElementById('tapeSellVol');
this.tradeCountEl = document.getElementById('tapeTradeCount');
this.volBuyBar = document.getElementById('tapeVolBuy');
this.volSellBar = document.getElementById('tapeVolSell');
// Wire up controls
this._initControls();
}
_initControls() {
// Auto-scroll toggle
const autoScrollBtn = document.getElementById('tapeAutoScroll');
autoScrollBtn.addEventListener('click', () => {
this.options.autoScroll = !this.options.autoScroll;
autoScrollBtn.classList.toggle('active', this.options.autoScroll);
});
// Clear button
document.getElementById('tapeClear').addEventListener('click', () => {
this.clear();
});
// Min size filter
document.getElementById('tapeMinSize').addEventListener('change', (e) => {
this.options.minSizeFilter = parseInt(e.target.value) || 1;
this._renderTrades();
});
// Side filter
document.getElementById('tapeSideFilter').addEventListener('change', (e) => {
this.options.sideFilter = e.target.value;
this._renderTrades();
});
this.options.minSizeFilter = 1;
this.options.sideFilter = 'all';
}
/**
* Add a new trade
* @param {Object} trade - { price, size, side, time, aggressor }
*/
addTrade(trade) {
if (!trade || !trade.price || !trade.size) return;
const normalizedTrade = {
price: trade.price,
size: trade.size,
side: trade.side || (trade.aggressor === 'buy' ? 'buy' : 'sell'),
time: trade.time || Date.now(),
aggressor: trade.aggressor || trade.side,
isBig: trade.size >= this.options.bigTradeThreshold
};
// Add to front of array
this.trades.unshift(normalizedTrade);
// Trim to max
if (this.trades.length > this.options.maxTrades) {
this.trades = this.trades.slice(0, this.options.maxTrades);
}
// Update cumulative volume
if (normalizedTrade.side === 'buy') {
this.cumulativeVolume.buy += normalizedTrade.size;
} else {
this.cumulativeVolume.sell += normalizedTrade.size;
}
this._renderTrades();
}
/**
* Add multiple trades at once
*/
addTrades(trades) {
if (!Array.isArray(trades)) return;
trades.forEach(t => this.addTrade(t));
}
/**
* Update big trade threshold dynamically
*/
setBigTradeThreshold(threshold) {
this.options.bigTradeThreshold = threshold;
// Re-tag existing trades
this.trades.forEach(t => {
t.isBig = t.size >= threshold;
});
this._renderTrades();
}
clear() {
this.trades = [];
this.cumulativeVolume = { buy: 0, sell: 0 };
this._renderTrades();
}
_renderTrades() {
// Filter trades
let filteredTrades = this.trades.filter(t => {
if (t.size < this.options.minSizeFilter) return false;
if (this.options.sideFilter !== 'all' && t.side !== this.options.sideFilter) return false;
return true;
});
// Limit display
filteredTrades = filteredTrades.slice(0, this.options.displayTrades);
// Generate HTML
let html = '';
filteredTrades.forEach(trade => {
const sideClass = trade.side === 'buy' ? 'tape-row-buy' : 'tape-row-sell';
const bigClass = trade.isBig ? 'tape-row-big' : '';
const time = this._formatTime(trade.time);
html += `
| ${time} |
${this._formatPrice(trade.price)} |
${this._formatSize(trade.size)} |
${trade.side.toUpperCase()}
|
`;
});
this.tbody.innerHTML = html;
// Update stats
this._updateStats();
// Auto-scroll to top (newest trades)
if (this.options.autoScroll && this.bodyEl) {
this.bodyEl.scrollTop = 0;
}
}
_updateStats() {
const buyVol = this.cumulativeVolume.buy;
const sellVol = this.cumulativeVolume.sell;
const totalVol = buyVol + sellVol;
this.buyVolEl.textContent = this._formatSize(buyVol);
this.sellVolEl.textContent = this._formatSize(sellVol);
this.tradeCountEl.textContent = this.trades.length.toString();
// Volume meter bars
const buyPct = totalVol > 0 ? (buyVol / totalVol) * 100 : 50;
this.volBuyBar.style.width = buyPct + '%';
this.volSellBar.style.width = (100 - buyPct) + '%';
}
_formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
_formatPrice(price) {
if (price >= 1000) return price.toFixed(1);
if (price >= 1) return price.toFixed(2);
return price.toFixed(4);
}
_formatSize(size) {
if (size >= 1000000) return (size / 1000000).toFixed(2) + 'M';
if (size >= 1000) return (size / 1000).toFixed(1) + 'K';
return Math.round(size).toString();
}
destroy() {
this.container.innerHTML = '';
}
}
// Export for use in main app
window.TimeAndSales = TimeAndSales;