// Trading Terminal Script - Mock microstructure with algorithmic bots // Light mode with primary green (#22c55e) and secondary red (#ef4444) (function () { const fmtUSD = (n) => { if (!isFinite(n)) return "-"; return n.toLocaleString(undefined, { style: "currency", currency: "USD", maximumFractionDigits: 2 }); }; const fmtNum = (n, d = 4) => (isFinite(n) ? n.toFixed(d) : "-"); const fmtSize = (n) => isFinite(n) ? n.toFixed(4) : "-"; const nowTs = () => new Date().toLocaleTimeString(); // State const state = { symbol: "BTCUSD", exchange: "BINANCE", lastPrice: 50000, dayChange: 0, dayOpen: 50000, spread: 1.0, bids: [], asks: [], tape: [], bots: [], pausedAll: false, latency: 2, // ms portfolio: { cash: 1_000_000, marginUsed: 0, positions: { BTCUSD: { qty: 0, avg: 0 }, ETHUSD: { qty: 0, avg: 0 }, SOLUSD: { qty: 0, avg: 0 }, }, }, }; // Initialize order book levels function initBook() { const mid = state.lastPrice; const rows = 24; // rows per side const step = 0.5; const bids = []; const asks = []; let bid = mid - rows * step; for (let i = 0; i < rows; i++, bid += step) { bids.push({ price: +(bid).toFixed(1), size: +(Math.random() * 2 + 0.1).toFixed(4) }); } let ask = mid + step; for (let i = 0; i < rows; i++, ask += step) { asks.push({ price: +(ask).toFixed(1), size: +(Math.random() * 2 + 0.1).toFixed(4) }); } state.bids = bids; state.asks = asks; } // Normalize book size to max function normalizeBook(max = 24) { function compress(levels) { if (levels.length <= max) return levels; const drop = levels.length - max; return levels.slice(drop); } state.bids = compress(state.bids); state.asks = compress(state.asks); } // Update microstructure: small random walk and depth changes function tickMicrostructure() { const drift = (Math.random() - 0.5) * 0.4; // mild drift state.lastPrice += drift; // Top of book drift if (state.bids.length && state.asks.length) { state.bids[state.bids.length - 1].price += drift * 0.8; state.asks[0].price += drift * 0.8; state.spread = +(state.asks[0].price - state.bids[state.bids.length - 1].price).toFixed(2); } // Randomly change sizes for (const side of ["bids", "asks"]) { for (let i = 0; i < state[side].length; i++) { const volDelta = (Math.random() - 0.5) * 0.05; state[side][i].size = Math.max(0.01, +(state[side][i].size + volDelta).toFixed(4)); } } // Recompute day change and open state.dayChange = +((state.lastPrice - state.dayOpen) / state.dayOpen * 100).toFixed(2); } // Add a trade to the tape function pushTrade(price, size, side, exchange = state.exchange) { state.tape.unshift({ time: nowTs(), price, size, side, exchange, }); if (state.tape.length > 200) state.tape.length = 200; } // Execute against our book and return filled price and size function simulateMarketOrder(side, qty) { let remaining = qty; let avgPrice = 0; let vol = 0; const bookSide = side === "buy" ? state.asks : state.bids; // buy hits asks, sell hits bids for (let i = 0; i < bookSide.length && remaining > 0; i++) { const level = bookSide[i]; const fill = Math.min(level.size, remaining); avgPrice += level.price * fill; vol += fill; remaining -= fill; // reduce level size after fill level.size = +(level.size - fill).toFixed(4); } if (vol > 0) avgPrice /= vol; // If book exhausted, use lastPrice const fillPrice = isFinite(avgPrice) && avgPrice > 0 ? avgPrice : state.lastPrice; // Trade in tape pushTrade(+fillPrice.toFixed(2), +vol.toFixed(4), side); // Portfolio impact (simple) const sym = state.symbol; const pos = state.portfolio.positions[sym]; if (!pos) return; if (side === "buy") { // Update avg price and qty const newQty = pos.qty + vol; const cost = pos.avg * pos.qty + fillPrice * vol; pos.avg = newQty > 0 ? cost / newQty : 0; pos.qty = newQty; } else { // sell const newQty = pos.qty - vol; const proceeds = pos.avg * pos.qty - fillPrice * vol; pos.avg = newQty > 0 ? (proceeds + fillPrice * vol) / newQty : pos.avg; // approx pos.qty = newQty; } updatePortfolioUI(); return { price: fillPrice, size: vol }; } // UI: Order Book function renderOrderBook() { const bidsEl = document.getElementById("bookBids"); const asksEl = document.getElementById("bookAsks"); const spreadEl = document.getElementById("bookSpread"); const tsEl = document.getElementById("bookTs"); bidsEl.innerHTML = ""; asksEl.innerHTML = ""; // Map sizes to gradient intensity const maxBidVol = state.bids.reduce((m, l) => Math.max(m, l.size), 0.001); const maxAskVol = state.asks.reduce((m, l) => Math.max(m, l.size), 0.001); // Bids for (const level of state.bids) { const intensity = level.size / maxBidVol; const row = document.createElement("div"); row.className = "grid grid-cols-3 px-3 py-1 hover:bg-indigo-500/10 rounded transition-all duration-200"; row.innerHTML = `
${fmtSize(level.size)}
${fmtNum(level.price, 1)}
-
`; // Gradient background bar row.style.background = `linear-gradient(to right, rgba(16,185,129,${0.15 * intensity}), rgba(16,185,129,0.02) 80%)`; bidsEl.appendChild(row); } // Asks for (const level of state.asks) { const intensity = level.size / maxAskVol; const row = document.createElement("div"); row.className = "grid grid-cols-3 px-3 py-1 hover:bg-indigo-500/10 rounded transition-all duration-200"; row.innerHTML = `
-
${fmtNum(level.price, 1)}
${fmtSize(level.size)}
`; row.style.background = `linear-gradient(to right, rgba(244,63,94,0.02) 20%, rgba(244,63,94,${0.15 * intensity}))`; asksEl.appendChild(row); } // Spread const bestBid = state.bids[state.bids.length - 1]?.price ?? state.lastPrice - state.spread / 2; const bestAsk = state.asks[0]?.price ?? state.lastPrice + state.spread / 2; spreadEl.innerHTML = `Spread: ${fmtNum(bestAsk - bestBid, 2)} | Best: ${fmtNum(bestBid, 1)} / ${fmtNum(bestAsk, 1)}`; tsEl.textContent = nowTs(); } // UI: Trade Tape function renderTape() { const body = document.getElementById("tapeBody"); body.innerHTML = state.tape .map( (t) => { const value = t.price * t.size; return `
${t.time}
${fmtNum(t.price, 2)}
${fmtSize(t.size)}
${t.side.toUpperCase()}
${fmtNum(value, 0)}
`; } ) .join(""); } // UI: Header stats function renderHeader() { const lastEl = document.getElementById("lastPrice"); const dayEl = document.getElementById("dayChange"); const spreadEl = document.getElementById("spread"); const midEl = document.getElementById("midPrice"); const symEl = document.getElementById("symbolTitle"); const exEl = document.getElementById("exchangeTitle"); const totalEl = document.getElementById("totalValue"); lastEl.textContent = fmtNum(state.lastPrice, 2); dayEl.textContent = `${state.dayChange >= 0 ? "+" : ""}${state.dayChange.toFixed(2)}%`; dayEl.classList.toggle("text-emerald-400", state.dayChange >= 0); dayEl.classList.toggle("text-rose-400", state.dayChange < 0); const bestBid = state.bids[state.bids.length - 1]?.price ?? state.lastPrice - state.spread / 2; const bestAsk = state.asks[0]?.price ?? state.lastPrice + state.spread / 2; const mid = (bestBid + bestAsk) / 2; spreadEl.textContent = fmtNum(bestAsk - bestBid, 2); if (midEl) midEl.textContent = fmtNum(mid, 2); if (symEl) symEl.textContent = state.symbol.replace("USD", "/USD"); if (exEl) exEl.textContent = state.exchange; // Update total portfolio value if (totalEl) { let totalValue = state.portfolio.cash; for (const [sym, pos] of Object.entries(state.portfolio.positions)) { const mark = getSymbolPrice(sym); if (pos.qty !== 0) { totalValue += mark * pos.qty; } } totalEl.textContent = fmtUSD(totalValue); } } // UI: Watchlist function renderWatchlist() { const items = document.querySelectorAll("#watchlist .wl-item"); items.forEach((el) => { const sym = el.getAttribute("data-symbol"); let price = state.lastPrice; let change = state.dayChange; // Use per-symbol price if selected if (sym !== state.symbol) { price = simulateSymbolPrice(sym); change = simulateSymbolChange(sym); } const priceEl = el.querySelector('[data-field="price"]'); const changeEl = el.querySelector('[data-field="change"]'); priceEl.textContent = fmtNum(price, 2); changeEl.textContent = `${change >= 0 ? "+" : ""}${change.toFixed(2)}%`; changeEl.classList.toggle("text-emerald-400", change >= 0); changeEl.classList.toggle("text-rose-400", change < 0); // Visual selection const container = el.querySelector('.relative > div:last-child'); container.classList.toggle("border-indigo-400", sym === state.symbol); container.classList.toggle("ring-1", sym === state.symbol); container.classList.toggle("ring-indigo-300", sym === state.symbol); container.classList.toggle("bg-indigo-500/10", sym === state.symbol); }); } // UI: Portfolio function updatePortfolioUI() { const pnlEl = document.getElementById("pnl"); const cashEl = document.getElementById("cash"); const marginEl = document.getElementById("marginUsed"); const holdingsEl = document.getElementById("holdings"); let unrealized = 0; for (const [sym, pos] of Object.entries(state.portfolio.positions)) { const mark = getSymbolPrice(sym); if (pos.qty !== 0) { unrealized += (mark - pos.avg) * pos.qty; } } pnlEl.textContent = fmtUSD(unrealized); pnlEl.classList.toggle("text-emerald-400", unrealized >= 0); pnlEl.classList.toggle("text-rose-400", unrealized < 0); cashEl.textContent = fmtUSD(state.portfolio.cash); marginEl.textContent = fmtUSD(state.portfolio.marginUsed); holdingsEl.innerHTML = Object.entries(state.portfolio.positions) .filter(([_, pos]) => pos.qty !== 0) .map( ([sym, pos]) => { const mark = getSymbolPrice(sym); const value = mark * pos.qty; const pnl = (mark - pos.avg) * pos.qty; return `
  • ${sym} ${fmtUSD(pnl)}
    ${pos.qty.toFixed(4)} ${fmtUSD(value)}
  • `; } ) .join(""); } function getSymbolPrice(sym) { if (sym === state.symbol) return state.lastPrice; // Mock other symbols around BTC with small offset const map = { ETHUSD: 3200, SOLUSD: 160 }; return map[sym] ?? 100; } function simulateSymbolPrice(sym) { return getSymbolPrice(sym) * (1 + (Math.random() - 0.5) * 0.002); } function simulateSymbolChange(sym) { const base = sym === "ETHUSD" ? 1.2 : sym === "SOLUSD" ? 2.1 : 0.8; return +((Math.random() - 0.5) * base).toFixed(2); } // Bots setup function initBots() { const container = document.getElementById("botsPanel"); container.innerHTML = ""; const bots = [ { id: "mm-btc", name: "Market Maker BTC", symbol: "BTCUSD", type: "Market Making", params: { skew: 0.05, quoteSize: 0.25, maxPositions: 2.0, a: 0.02, b: 0.01, minEdge: 0.2, }, color: "green", }, { id: "twap-eth", name: "TWAP ETH", symbol: "ETHUSD", type: "TWAP", params: { side: "buy", intervalMs: 2000, sliceQty: 0.4, remaining: 8.0, }, color: "blue", }, { id: "arb-sol", name: "Cross-venue Arb SOL", symbol: "SOLUSD", type: "Arbitrage", params: { threshold: 0.6, qty: 0.5, }, color: "purple", }, { id: "trend-btc", name: "Trend Follower BTC", symbol: "BTCUSD", type: "Momentum", params: { window: 20, threshold: 0.6, qty: 0.8, }, color: "amber", }, ]; bots.forEach((b) => { const el = document.createElement("bot-card"); el.bot = b; container.appendChild(el); }); state.bots = bots; } // Bot engine loop function botEngineLoop() { const container = document.getElementById("botsPanel"); const cards = Array.from(container.querySelectorAll("bot-card")); const getBotById = (id) => state.bots.find((b) => b.id === id); for (const card of cards) { const bot = getBotById(card.bot.id); if (!bot || bot.paused) continue; switch (bot.type) { case "Market Making": marketMaker(bot, card); break; case "TWAP": twap(bot, card); break; case "Arbitrage": arbitrage(bot, card); break; case "Momentum": momentum(bot, card); break; } card.updateUI(); } } // Bot strategies (simplified and mocked) function marketMaker(bot, card) { // MM places quotes around mid with a small skew. // Skew buy quotes slightly lower and sell quotes slightly higher based on inventory. const sym = bot.symbol; if (sym !== state.symbol) return; // only active for selected symbol const pos = state.portfolio.positions[sym] || { qty: 0 }; const inv = pos.qty; const bestBid = state.bids[state.bids.length - 1]?.price ?? state.lastPrice - state.spread / 2; const bestAsk = state.asks[0]?.price ?? state.lastPrice + state.spread / 2; const mid = (bestBid + bestAsk) / 2; const baseBid = mid - bot.params.minEdge; const baseAsk = mid + bot.params.minEdge; const skew = bot.params.skew; // Place "internal" quotes at base +/- skew relative to inventory const fakeBidPrice = baseBid - inv * skew; const fakeAskPrice = baseAsk - inv * skew; // "Hitting" logic: if our fake quotes are better than top-of-book by some edge, simulate execution const edge = Math.random() * 0.05; // 0-5 cents randomness if (Math.random() < 0.6) { // 60% chance to hit a side randomly const side = Math.random() < 0.5 ? "buy" : "sell"; const edgePrice = side === "buy" ? fakeAskPrice - edge : fakeBidPrice + edge; const top = side === "buy" ? state.asks[0]?.price : state.bids[state.bids.length - 1]?.price; if (top && Math.abs(edgePrice - top) < 0.8) { simulateMarketOrder(side, bot.params.quoteSize); card.log(`${side.toUpperCase()} filled ${bot.params.quoteSize} @ ${fmtNum(state.lastPrice, 2)}`); } } card.setQuotes({ bid: fakeBidPrice, ask: fakeAskPrice, spread: fakeAskPrice - fakeBidPrice, }); } function twap(bot, card) { if (!bot._next) bot._next = performance.now() + bot.params.intervalMs; const now = performance.now(); if (now >= bot._next) { bot._next = now + bot.params.intervalMs; const qty = Math.min(bot.params.sliceQty, bot.params.remaining); if (qty > 0) { simulateMarketOrder(bot.params.side, qty); bot.params.remaining = +(bot.params.remaining - qty).toFixed(4); card.log(`${bot.params.side.toUpperCase()} ${qty} via TWAP @ ${fmtNum(state.lastPrice, 2)}`); } else { card.log(`TWAP complete`); bot.paused = true; } } card.setProgress((1 - bot.params.remaining / 8.0) * 100); } function arbitrage(bot, card) { // Simulate cross-venue: if spread exceeds threshold, flip side const threshold = bot.params.threshold; const bestBid = state.bids[state.bids.length - 1]?.price ?? state.lastPrice - state.spread / 2; const bestAsk = state.asks[0]?.price ?? state.lastPrice + state.spread / 2; const spread = bestAsk - bestBid; if (spread > threshold && Math.random() < 0.3) { simulateMarketOrder("buy", bot.params.qty); simulateMarketOrder("sell", bot.params.qty); card.log(`ARB ${bot.params.qty} both sides @ spread ${fmtNum(spread, 2)}`); } card.setMetric("Spread", fmtNum(spread, 2)); } function momentum(bot, card) { // Very simple: compute recent fake momentum from lastPrice diffs if (!bot._window) { bot._window = []; bot._next = performance.now() + 500; } const now = performance.now(); if (now >= bot._next) { bot._next = now + 500; bot._window.push(state.lastPrice); if (bot._window.length > bot.params.window) bot._window.shift(); } const arr = bot._window; if (arr.length >= 2) { const delta = arr[arr.length - 1] - arr[0]; if (Math.abs(delta) > bot.params.threshold) { const side = delta > 0 ? "buy" : "sell"; simulateMarketOrder(side, bot.params.qty); card.log(`${side.toUpperCase()} momentum ${fmtNum(delta, 2)}`); } } card.setMetric("Window", bot.params.window); } // Event: select symbol in watchlist function wireWatchlist() { document.querySelectorAll("#watchlist .wl-item").forEach((el) => { el.addEventListener("click", () => { const sym = el.getAttribute("data-symbol"); if (sym === state.symbol) return; state.symbol = sym; state.dayOpen = state.lastPrice = getSymbolPrice(sym); state.exchange = "BINANCE"; initBook(); renderHeader(); renderWatchlist(); initBots(); // re-init bots for simplicity }); }); } // Top-level loop function tick() { if (!state.pausedAll) { tickMicrostructure(); renderHeader(); renderOrderBook(); renderWatchlist(); renderTape(); botEngineLoop(); } } // Controls function wireControls() { const pauseBtn = document.getElementById("pauseAllBtn"); pauseBtn.addEventListener("click", () => { state.pausedAll = !state.pausedAll; pauseBtn.innerHTML = state.pausedAll ? ` Resume` : ` Pause`; feather.replace(); }); const refreshBtn = document.getElementById("refreshBtn"); if (refreshBtn) { refreshBtn.addEventListener("click", () => { state.dayOpen = state.lastPrice; renderHeader(); renderWatchlist(); }); } } // Initialize function init() { wireWatchlist(); wireControls(); initBook(); initBots(); renderHeader(); renderOrderBook(); renderTape(); renderWatchlist(); updatePortfolioUI(); // Preload tape with some trades for (let i = 0; i < 12; i++) { const side = Math.random() < 0.5 ? "buy" : "sell"; pushTrade(state.lastPrice + (Math.random() - 0.5) * 2, Math.random() * 1.5 + 0.2, side); } renderTape(); setInterval(tick, 50); // ~50ms } // Start init(); })();