// Walls in the live book, and how each one ended: filled (traded away) or pulled (gone with no
// trades at its price). A wall that moves a few ticks is followed, not marked. The book is live
// only, so the marks start when the chart opens.
indicator('Book walls', { overlay: true });
const wallUsd = input.float('Wall (USD)', 1_000_000, { min: 10_000, step: 50_000 });
// Market makers move size around all the time; a wall counts once it has rested this long.
const minAge = input.int('Rested at least (s)', 60, { min: 0, max: 3_600 });
const shown = input.int('Walls a side', 3, { min: 1, max: 10 });
// A wall that leaves its price and turns up on its side this close, this soon, is the same wall moving.
const MOVE_PCT = 0.1;
const MOVE_MS = 3_000;
const chart = bars();
// Trades first: a block's trades then come before the book that block left.
const tape = trades();
const b = book({ levels: 400 });
// Size traded at each price since the book last changed.
const traded = state.map<number>(1_024, 'live');
// When each wall began resting, by side and price; a wall that moves keeps its time.
const since = state.map<number>(2_048, 'live');
// Walls that arrived at their price in the last MOVE_MS: one that left nearby may have moved there.
const arrived = state.map<number>(256, 'live');
// Walls gone with no trades in the last MOVE_MS, not yet known to be pulled: when, how much, resting since.
const goneAt = state.map<number>(256, 'live');
const goneUsd = state.map<number>(256, 'live');
const goneSince = state.map<number>(256, 'live');
// The walls that ended this bar, by how, side and price, in USD.
const ended = tallyBy(256);
const marks = plot.markers('Wall ended');
const keyOf = (side: string, price: number) => `${side}:${price}`;
const sideOf = (key: string) => (key.startsWith('bid') ? 'bid' : 'ask');
const priceOf = (key: string) => Number(key.slice(4));
// The key in `walls` on `side` nearest `price`, within MOVE_PCT, that `fits`; null if none.
const nearest = (walls: typeof arrived, side: string, price: number, fits: (key: string) => boolean) => {
let best: string | null = null;
let bestGap = (price * MOVE_PCT) / 100;
for (const [key] of walls.entries()) {
if (sideOf(key) !== side) continue;
const gap = Math.abs(priceOf(key) - price);
if (gap <= bestGap && fits(key)) {
best = key;
bestGap = gap;
}
}
return best;
};
const forget = (key: string) => {
goneAt.delete(key);
goneUsd.delete(key);
goneSince.delete(key);
};
tape.onTrade((t) => {
// Older bars have trades but no book: nothing to match them to.
if (!b.available) return;
const key = String(t.price);
traded.set(key, (traded.get(key) ?? 0) + t.size);
});
b.onUpdate((u) => {
if (u.snapshot) {
// A new book: its walls rest from now, as far as anyone can tell.
since.clear();
arrived.clear();
goneAt.clear();
goneUsd.clear();
goneSince.clear();
for (const l of u.bids) if (l.notional >= wallUsd) since.set(keyOf('bid', l.price), u.time);
for (const l of u.asks) if (l.notional >= wallUsd) since.set(keyOf('ask', l.price), u.time);
traded.clear();
return;
}
// A wall gone with no wall turning up near it was pulled.
for (const [key, at] of [...goneAt.entries()]) {
if (u.time - at <= MOVE_MS) continue;
const start = goneSince.get(key) ?? at;
if (at - start >= minAge * 1_000) ended.add(`pulled:${key}`, goneUsd.get(key) ?? 0);
forget(key);
}
for (const [key, at] of [...arrived.entries()]) if (u.time - at > MOVE_MS) arrived.delete(key);
// Walls that left the levels held are still listed: once the list is long, drop what is no wall now.
if (since.size >= 2_000) {
for (const [key] of [...since.entries()]) {
if (!(u.size(sideOf(key), priceOf(key)) * priceOf(key) >= wallUsd)) since.delete(key);
}
}
for (const c of u.changes) {
const key = keyOf(c.side, c.price);
const was = c.previous * c.price >= wallUsd;
const is = c.size * c.price >= wallUsd;
if (!was) {
if (!is) continue;
// A wall arriving where one just left nearby is that wall, moved: it keeps its time.
const from = nearest(goneAt, c.side, c.price, (k) => c.size * c.price >= (goneUsd.get(k) ?? 0) / 2);
if (from !== null) {
since.set(key, goneSince.get(from) ?? u.time);
forget(from);
} else if (since.size < 2_000) since.set(key, u.time);
if (arrived.size < 250) arrived.set(key, u.time);
continue;
}
// Still most of it there: the wall stands, unless it is now too small to be one.
if (c.size > c.previous / 2) {
if (!is) since.delete(key);
continue;
}
const start = since.get(key);
since.delete(key);
arrived.delete(key);
const gone = c.previous - c.size;
if ((traded.get(String(c.price)) ?? 0) >= gone / 2) {
if (start !== undefined && u.time - start >= minAge * 1_000) ended.add(`filled:${key}`, gone * c.price);
continue;
}
if (start === undefined) continue;
// Gone with no trades: moved, if a wall as big just arrived near it; else pulled, unless one arrives soon.
const to = nearest(arrived, c.side, c.price, (k) => u.size(sideOf(k), priceOf(k)) * priceOf(k) >= (gone * c.price) / 2);
if (to !== null) {
since.set(to, Math.min(start, since.get(to) ?? start));
arrived.delete(to);
} else if (goneAt.size < 250) {
goneAt.set(key, u.time);
goneUsd.set(key, gone * c.price);
goneSince.set(key, start);
}
}
traded.clear();
});
chart.onBar(() => {
for (const [key, usd] of ended.entries()) {
const [how, side, price] = key.split(':');
marks.add({
position: 'at',
price: Number(price),
shape: how === 'filled' ? 'xcross' : 'circle',
color: side === 'bid' ? color.up : color.down,
text: `${how === 'filled' ? 'F' : 'P'} ${fmt.usd(usd)}`,
});
}
// The walls resting now, on the bar still forming: drawn again with each update.
if (chart.confirmed || !b.available) return;
const walls = (levels: readonly BookLevel[], tint: Color) => {
const big = levels.filter((l) => l.notional >= wallUsd);
big.sort((x, y) => y.notional - x.notional);
for (const l of big.slice(0, shown)) {
draw.line({ from: { bar: chart.index - 20, price: l.price }, to: { bar: chart.index, price: l.price }, color: tint, width: 2, extend: 'right' });
draw.label({ at: { bar: chart.index + 2, price: l.price }, text: fmt.usd(l.notional), style: 'left', color: color.panel, textColor: tint, size: 'small' });
}
};
walls(b.bids, color.up);
walls(b.asks, color.down);
});