Files
BlackboxAI 0206ef7cbb Initial commit: orderflow analysis system with 5 pattern detectors
Real-time orderflow trading system with absorption, initiative, sweep,
exhaustion, and divergence detection. Features volume profile framing,
state machine trade lifecycle, MT5 + Bybit feeds, FastAPI dashboard,
and Telegram alerts for 30+ instruments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:38:25 +03:00

319 lines
11 KiB
JavaScript

/**
* Orderbook Depth Ladder Component
* Real-time DOM (Depth of Market) ladder display
* Shows bid/ask sizes with imbalance highlighting
*/
class OrderbookLadder {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = {
levels: 20, // Number of price levels to show
priceStep: 0.5, // Tick size
updateThrottle: 100, // ms between renders
colors: {
background: '#1c2128',
bidBar: '#3fb950',
askBar: '#f85149',
bidText: '#3fb950',
askText: '#f85149',
priceText: '#e6edf3',
currentPrice: '#58a6ff',
thinLevel: '#d29922',
gridLine: '#30363d',
imbalanceBid: 'rgba(63, 185, 80, 0.3)',
imbalanceAsk: 'rgba(248, 81, 73, 0.3)',
...options.colors
},
...options
};
this.bids = []; // [ {price, size} ]
this.asks = []; // [ {price, size} ]
this.currentPrice = null;
this.lastTrade = null;
this.thinLevels = []; // Price levels with thin liquidity (sweep targets)
this.maxSize = 0; // For bar normalization
this._lastRender = 0;
this._pendingRender = false;
this._init();
}
_init() {
this.container.innerHTML = `
<div class="ob-ladder">
<div class="ob-header">
<span class="ob-col-bid">BID SIZE</span>
<span class="ob-col-price">PRICE</span>
<span class="ob-col-ask">ASK SIZE</span>
</div>
<div class="ob-body" id="obLadderBody"></div>
<div class="ob-footer">
<div class="ob-imbalance-gauge">
<div class="ob-imb-bar" id="obImbBar"></div>
</div>
<div class="ob-stats">
<span class="ob-stat">
<span class="ob-stat-label">Bid Total:</span>
<span class="ob-stat-value ob-bid" id="obBidTotal">--</span>
</span>
<span class="ob-stat">
<span class="ob-stat-label">Ask Total:</span>
<span class="ob-stat-value ob-ask" id="obAskTotal">--</span>
</span>
<span class="ob-stat">
<span class="ob-stat-label">Ratio:</span>
<span class="ob-stat-value" id="obRatio">--</span>
</span>
</div>
</div>
</div>
`;
this.bodyEl = document.getElementById('obLadderBody');
this.imbBarEl = document.getElementById('obImbBar');
this.bidTotalEl = document.getElementById('obBidTotal');
this.askTotalEl = document.getElementById('obAskTotal');
this.ratioEl = document.getElementById('obRatio');
}
/**
* Update full orderbook snapshot
* @param {Object} data - { bids: [{price, size}], asks: [{price, size}], currentPrice }
*/
setData(data) {
if (!data) return;
this.bids = data.bids || [];
this.asks = data.asks || [];
this.currentPrice = data.currentPrice || data.last_price || null;
// Sort: bids descending, asks ascending
this.bids.sort((a, b) => b.price - a.price);
this.asks.sort((a, b) => a.price - b.price);
// Calculate max size for normalization
this._calculateMaxSize();
// Detect thin levels
this._detectThinLevels();
this._scheduleRender();
}
/**
* Update single level (real-time delta)
*/
updateLevel(side, price, size) {
const levels = side === 'bid' ? this.bids : this.asks;
const idx = levels.findIndex(l => l.price === price);
if (size === 0) {
// Remove level
if (idx >= 0) levels.splice(idx, 1);
} else if (idx >= 0) {
// Update existing
levels[idx].size = size;
} else {
// Insert new level
levels.push({ price, size });
if (side === 'bid') {
this.bids.sort((a, b) => b.price - a.price);
} else {
this.asks.sort((a, b) => a.price - b.price);
}
}
this._calculateMaxSize();
this._scheduleRender();
}
/**
* Update current price (from trade)
*/
updatePrice(price, side = null) {
this.currentPrice = price;
this.lastTrade = { price, side, time: Date.now() };
this._scheduleRender();
}
_calculateMaxSize() {
const allSizes = [
...this.bids.slice(0, this.options.levels).map(l => l.size),
...this.asks.slice(0, this.options.levels).map(l => l.size)
];
this.maxSize = Math.max(...allSizes, 1);
}
_detectThinLevels() {
// Find levels with significantly lower liquidity (sweep targets)
const allLevels = [
...this.bids.slice(0, this.options.levels),
...this.asks.slice(0, this.options.levels)
];
if (allLevels.length < 3) return;
const avgSize = allLevels.reduce((s, l) => s + l.size, 0) / allLevels.length;
const thinThreshold = avgSize * 0.3;
this.thinLevels = allLevels
.filter(l => l.size < thinThreshold)
.map(l => l.price);
}
_scheduleRender() {
if (this._pendingRender) return;
const now = Date.now();
const elapsed = now - this._lastRender;
if (elapsed >= this.options.updateThrottle) {
this._render();
} else {
this._pendingRender = true;
setTimeout(() => {
this._pendingRender = false;
this._render();
}, this.options.updateThrottle - elapsed);
}
}
_render() {
this._lastRender = Date.now();
if (!this.bodyEl) return;
// Build unified price ladder centered on current price
const levels = this._buildLadder();
// Generate HTML
let html = '';
levels.forEach(level => {
const isCurrent = level.price === this.currentPrice;
const isThin = this.thinLevels.includes(level.price);
const bidPct = level.bidSize ? (level.bidSize / this.maxSize) * 100 : 0;
const askPct = level.askSize ? (level.askSize / this.maxSize) * 100 : 0;
// Imbalance detection
const hasImbalance = level.bidSize && level.askSize &&
(level.bidSize > level.askSize * 3 || level.askSize > level.bidSize * 3);
const imbClass = hasImbalance
? (level.bidSize > level.askSize ? 'ob-imb-bid' : 'ob-imb-ask')
: '';
html += `
<div class="ob-row ${isCurrent ? 'ob-current' : ''} ${isThin ? 'ob-thin' : ''} ${imbClass}">
<div class="ob-cell ob-bid-cell">
${level.bidSize ? `
<div class="ob-bar ob-bid-bar" style="width: ${bidPct}%"></div>
<span class="ob-size ob-bid">${this._formatSize(level.bidSize)}</span>
` : ''}
</div>
<div class="ob-cell ob-price-cell ${isCurrent ? 'ob-price-current' : ''}">
${this._formatPrice(level.price)}
</div>
<div class="ob-cell ob-ask-cell">
${level.askSize ? `
<div class="ob-bar ob-ask-bar" style="width: ${askPct}%"></div>
<span class="ob-size ob-ask">${this._formatSize(level.askSize)}</span>
` : ''}
</div>
</div>
`;
});
this.bodyEl.innerHTML = html;
// Update stats
this._updateStats();
// Scroll to center on current price
this._scrollToCurrentPrice();
}
_buildLadder() {
const levels = [];
const numLevels = this.options.levels;
const step = this.options.priceStep;
// Get all prices we have data for
const bidMap = new Map(this.bids.map(b => [b.price, b.size]));
const askMap = new Map(this.asks.map(a => [a.price, a.size]));
// Determine center price
const centerPrice = this.currentPrice ||
(this.bids.length > 0 && this.asks.length > 0
? (this.bids[0].price + this.asks[0].price) / 2
: 0);
if (centerPrice === 0) return levels;
// Build ladder around center price
const halfLevels = Math.floor(numLevels / 2);
for (let i = halfLevels; i >= -halfLevels; i--) {
const price = this._roundPrice(centerPrice + i * step);
levels.push({
price,
bidSize: bidMap.get(price) || 0,
askSize: askMap.get(price) || 0
});
}
return levels;
}
_roundPrice(price) {
const step = this.options.priceStep;
return Math.round(price / step) * step;
}
_updateStats() {
const bidTotal = this.bids.slice(0, this.options.levels).reduce((s, l) => s + l.size, 0);
const askTotal = this.asks.slice(0, this.options.levels).reduce((s, l) => s + l.size, 0);
const ratio = askTotal > 0 ? bidTotal / askTotal : 0;
this.bidTotalEl.textContent = this._formatSize(bidTotal);
this.askTotalEl.textContent = this._formatSize(askTotal);
this.ratioEl.textContent = ratio.toFixed(2) + 'x';
this.ratioEl.className = 'ob-stat-value ' + (ratio > 1.2 ? 'ob-bid' : ratio < 0.8 ? 'ob-ask' : '');
// Imbalance gauge
const total = bidTotal + askTotal;
const bidPct = total > 0 ? (bidTotal / total) * 100 : 50;
this.imbBarEl.style.width = bidPct + '%';
this.imbBarEl.className = 'ob-imb-bar ' + (bidPct > 55 ? 'ob-imb-bid' : bidPct < 45 ? 'ob-imb-ask' : '');
}
_scrollToCurrentPrice() {
if (!this.currentPrice || !this.bodyEl) return;
const currentRow = this.bodyEl.querySelector('.ob-current');
if (currentRow) {
currentRow.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}
_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.OrderbookLadder = OrderbookLadder;