// 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();
})();