TradeFeed
Time and sales: trades as they happen, newest first, buys green and sells red, large ones picked out, capped at a number of rows.
Demo
| Time | Price | Size |
|---|---|---|
| 14:30:13 | 61,261.6 sell | 0.36 BTC |
| 14:30:12 | 61,263.1 sell | 0.22 BTC |
| 14:30:11 | 61,275.7 buy | 0.14 BTC |
| 14:30:10 | 61,259.5 sell | 0.13 BTC |
| 14:30:09 | 61,294.5 buy | 0.17 BTC |
| 14:30:08 | 61,260.5 sell | 0.28 BTC |
| 14:30:07 | 61,277.7 buy | 0.44 BTC |
| 14:30:06 | 61,266.1 sell | 0.07 BTC |
| 14:30:05 | 61,265.7 sell | 0.35 BTC |
| 14:30:04 | 61,276.6 buy | 0.1 BTC |
| 14:30:03 | 61,259.0 sell | 0.5 BTC |
| 14:30:02 | 61,252.8 sell | 0.37 BTC |
A trade every 0.6 s (14 so far); trades of 1.5 BTC or more stand out.
| Time | Price | Size |
|---|---|---|
| 15:59:59 | $182.39 sell | 25,000 |
| 15:59:58 | $182.39 sell | 100 |
| 15:59:57 | $182.41 buy | 1,200 |
| 15:59:56 | $182.40 buy | 300 |
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/trade_feed.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/trade_feed.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/trade_feed.qwe
// The next trade of a seeded random walk (Park–Miller): the same on the server and in// the browser. Trades come a second apart from 14:30:00 on 2026-10-28.fn trade_feed_demo_trade(n: int) -> map { let s = (n * n * 7919 + n * 104729 + 1) % 2147483647 let rnd = () => { s = s * 16807 % 2147483647 s / 2147483647 } rnd() let up = rnd() let size = round((rnd() < 0.08 ? 1.5 + rnd() * 3 : 0.01 + rnd() * 0.6) * 1000) / 1000 { id: n, time: 1793197800000 + n * 1000, price: round((61250 + (up - 0.5) * 40 + math.sin(n / 6) * 25) * 10) / 10, size, side: up >= 0.5 ? "buy" : "sell" }}component TradeFeedDemoLive() { state trades = for n in range(14) => trade_feed_demo_trade(n) state live = true if live { every(600, () => { trades = trades + [trade_feed_demo_trade(len(trades))] }) } <div.demo-stack id="trade_feed-demo-live"> <TradeFeed trades={trades} limit={12} large={1.5} unit=" BTC" decimals={1} height={300} label="BTC-USD trades"/> <p.demo-note> A trade every 0.6 s ({len(trades)} so far); trades of 1.5 BTC or more stand out. </p> <div.demo-row> <Button size="sm" variant="outline" id="trade_feed-demo-toggle" on_click={() => { live = !live }}>{live ? "Stop the feed" : "Start the feed"}</Button> </div> </div>}view TradeFeedDemo() = <div.demo-stack> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px"> <TradeFeedDemoLive/> <div id="trade_feed-demo-still"> <TradeFeed label="ACME trades" prefix="$" limit={5} trades={[ { id: "a", time: "2026-10-28T15:59:56Z", price: 182.4, size: 300, side: "buy" }, { id: "b", time: "2026-10-28T15:59:57Z", price: 182.41, size: 1200, side: "buy" }, { id: "c", time: "2026-10-28T15:59:58Z", price: 182.39, size: 100, side: "sell" }, { id: "d", time: "2026-10-28T15:59:59Z", price: 182.39, size: 25000, side: "sell" }]} large={10000}/> </div> </div> </div>Source
ui/trade_feed.qwe
// TradeFeed: time and sales: trades as they happen, newest at the top, buys in green// and sells in red, an arrow where the price ticked up or down, large trades picked// out, and only the last `limit` kept. Each new row flashes once as it comes in. A// Pause button holds the list still to read it (new trades are counted meanwhile).//// <TradeFeed trades={trades} limit={40} large={1} unit=" BTC" decimals={1}/>// component Tape() {// state trades = []// if live { every(500, () => { trades = trades + [next_trade()] }) }// <TradeFeed trades={trades} prefix="$"/>// }//// trades: a list of { id, time, price, size, side }, oldest first, as they come (add new// ones at the end): time a date-time ("2026-10-28T14:30:05Z") or milliseconds since// 1970, shown as HH:MM:SS (UTC); side "buy" or "sell"; id tells trades apart (time,// price and size by default). limit: how many rows are shown (30). large: a size from// which a trade is picked out. prefix and suffix: around prices ("$"); decimals: digits// after the point of prices (2); size_decimals: of sizes (up to 4, as needed); unit:// after sizes (" BTC"). height: the list's height in pixels before it scrolls (320).// label: what screen readers call the table ("Trades").import ui/theme.qwe use ui_clslet trade_feed_css = css`.ui-tfeed { display: grid; gap: 8px; min-width: 0; font: 13px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-tfeed-bar { display: flex; align-items: center; gap: 10px; color: var(--ui-muted); font-size: 12px; }.ui-tfeed-bar span { margin-right: auto; }.ui-tfeed-pause { height: 26px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-tfeed-pause:hover { border-color: var(--ui-strong); }.ui-tfeed-pause:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-tfeed-pause[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-tfeed-box { max-height: var(--h); overflow-y: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); }.ui-tfeed-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }.ui-tfeed-table th { position: sticky; top: 0; z-index: 1; padding: 6px 10px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .04em; text-align: right; text-transform: uppercase; }.ui-tfeed-table td { padding: 4px 10px; text-align: right; white-space: nowrap; }.ui-tfeed-table th:first-child, .ui-tfeed-table td:first-child { text-align: left; }.ui-tfeed-time { color: var(--ui-muted); }.ui-tfeed-buy { --k: var(--ui-success); } .ui-tfeed-sell { --k: var(--ui-danger); }.ui-tfeed-price { color: var(--k); font-weight: 600; }.ui-tfeed-tick { display: inline-block; width: 10px; color: var(--ui-muted); font-size: 10px; }.ui-tfeed-large td { background: color-mix(in srgb, var(--k) 10%, transparent); font-weight: 650; }.ui-tfeed-large td:first-child { box-shadow: inset 3px 0 0 var(--k); }.ui-tfeed-new td { animation: ui-tfeed-flash 1s ease-out; }@keyframes ui-tfeed-flash { from { background: color-mix(in srgb, var(--k) 30%, transparent); } }.ui-tfeed-empty td { padding: 18px; color: var(--ui-muted); text-align: center !important; }@media (prefers-reduced-motion: reduce) { .ui-tfeed-new td { animation: none; } }`// n (a whole number, 0 or more) with at least `width` digits.fn trade_feed_pad(n: int, width: int) -> str { let s = str(n) len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn trade_feed_group(n: int) -> str = n < 1000 ? str(n) : trade_feed_group(floor(n / 1000)) + "," + trade_feed_pad(n % 1000, 3)// A value as it reads, with commas and `places` digits after the point (up to two as// needed when that is null): 48210.5 is "48,210.50" with 2. Worked out in whole// numbers, so the server and the browser write the same text.fn trade_feed_num(v: num, places: int? = null) -> str { let digits = places == null ? 2 : (places < 0 ? 0 : (places > 6 ? 6 : places)) let scale = [1, 10, 100, 1000, 10000, 100000, 1000000][digits] let units = round(abs(v) * scale) let frac = units % scale let tail = digits == 0 || (places == null && frac == 0) ? "" : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${trade_feed_pad(frac, digits)}`) `${v < 0 && units > 0 ? "-" : ""}${trade_feed_group(floor(units / scale))}${tail}`}// A trade's time as HH:MM:SS: of a date-time as written, of milliseconds in UTC.fn trade_feed_clock(t) -> str { let text = type_of(t) == "str" ? t : (t == null ? "" : date.from_ms(t, "UTC")) len(text) >= 19 ? slice(text, 11, 19) : text}component TradeFeed(trades: list = [], limit: int = 30, large: num? = null, prefix: str = "", suffix: str = "", decimals: int = 2, size_decimals: int? = null, unit: str = "", height: int = 320, label: str = "Trades", class: str? = null) { style global trade_feed_css state paused = false state held = [] // The trades the page started with, which do not flash. state first = trades | map((t) => t.id ?? `${t.time}-${t.price}-${t.size}`) let source = paused ? held : trades let count = len(source) let keep = limit < 1 ? 1 : limit let recent = drop(source, count > keep ? count - keep : 0) let rows = reverse(for [i, t] in enumerate(recent) => { let before = i > 0 ? recent[i - 1] : (count > keep ? source[count - keep - 1] : null) { ...t, key: t.id ?? `${t.time}-${t.price}-${t.size}`, tick: before == null || before.price == t.price ? 0 : (t.price > before.price ? 1 : -1) } }) let waiting = paused ? len(trades) - len(held) : 0 let price = (v) => `${prefix}${trade_feed_num(v, decimals)}${suffix}` let size = (v) => `${trade_feed_num(v, size_decimals)}${unit}` <div class={ui_cls(["ui-tfeed", class])} style={`--h: ${height}px`}> <div.ui-tfeed-bar> <span role="status">{paused ? `Paused · ${waiting} new` : `Last ${len(rows)} of ${count}`}</span> <button.ui-tfeed-pause type="button" aria-pressed={paused ? "true" : "false"} @click={() => { if !paused { held = trades } paused = !paused }}>{paused ? "Resume" : "Pause"}</button> </div> <div.ui-tfeed-box tabindex="0" role="region" aria-label={label}> <table.ui-tfeed-table> <thead><tr><th scope="col">Time</th><th scope="col">Price</th><th scope="col">Size</th></tr></thead> <tbody> {len(rows) == 0 && <tr.ui-tfeed-empty><td colspan="3">No trades yet.</td></tr>} {for r in rows => <tr key={r.key} data-id={r.key} class={ui_cls([r.side == "sell" ? "ui-tfeed-sell" : "ui-tfeed-buy", large != null && r.size >= large && "ui-tfeed-large"])} @mount={(el) => { if index_of(first, r.key) == null { el.classList.add("ui-tfeed-new") } }}> <td.ui-tfeed-time>{trade_feed_clock(r.time)}</td> <td.ui-tfeed-price> <span.ui-tfeed-tick aria-hidden="true">{r.tick > 0 ? "▲" : (r.tick < 0 ? "▼" : "")}</span>{price(r.price)} <span.ui-sr>{r.side == "sell" ? " sell" : " buy"}</span> </td> <td>{size(r.size)}</td> </tr>} </tbody> </table> </div> </div>}Related
OrderBookAn order book's bid and ask ladders with size bars, the spread and mid price, grouping into coarser steps, and levels that flash as they change.6 views · 6 downloadsPortfolioAllocationA donut of a portfolio's holdings beside a table of each one's weight, value and gain, with the totals.6 views · 6 downloadsWatchlistSymbols with last price, change, percent and a sparkline, in columns that sort, with prices that flash as they change.6 views · 6 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsCandlestickChartOHLC candles, green up and red down, with a volume pane, a date axis, moving averages and the details of each candle on hover.5 views · 7 downloadsDepthChartAn order book's bids and asks as stepped areas of the size waiting at each price or better, around the mid price, with the spread.5 views · 8 downloads
Thread
Thread 0 posts
On TradeFeed: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.