* docs: design for Hyperliquid trading (crypto + HIP-3 stock perps) Design only, no feature code. Covers custody (client-side API wallet signing, recommended), agent approve/revoke flow, asset routing for core/spot/HIP-3, order ticket, user streams for POS/ORD/ACTV, safety rails, testnet-only test plan, phases, and open questions including the Hyperliquid ToS restriction on Ontario. * docs(hl-trading): plan the market key so HL perp rows reprice and trade Greptile: market_key() returns None for Future and live_price/row actions depend on it. Add Position.market and the portfolio_view work. * docs(hl-trading): keep testnet and mainnet quotes in separate maps Greptile: same market keys on both networks would let one overwrite the other in the shared quote map. Plan a network-aware live_price and mainnet-only gateway requests, with interleaved-update tests. * feat(hl): add tr-hl, a testnet-only Hyperliquid signing and trading crate tr-hl signs and sends Hyperliquid actions from the device. It covers L1 phantom-agent signing and user-signed approveAgent, the order, cancel, cancelByCloid, modify, updateLeverage, scheduleCancel and noop actions, asset ids read from perpDexs and meta, tick and lot rounding, info and exchange clients, a user-stream reducer and a nonce manager. Only testnet exists in this crate. The endpoints are the testnet hosts, the phantom agent source is always "b", hyperliquidChain is always "Testnet", and Network has one variant. A test greps the crate for mainnet hosts. The caps are constants and the trader enforces them before it signs. Orders are capped at $100 notional, leverage at 3x and market slippage at 1%. The builder field is never sent. Signatures match the Python SDK's published vectors, plus an approveAgent vector generated with SDK 0.24.0. The crate builds for wasm32. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat: Hyperliquid testnet trading in the app Hyperliquid joins LIVE next to SnapTrade, with its own opt-in in Settings > Trading > Hyperliquid and an amber TESTNET badge on the toolbar, the account, the ticket and every confirmation. - Connect an address read-only. The account's positions, orders, fills and funding then show in PORT, POS, ORD and ACTV, from Hyperliquid's websocket and never through Terminal's server. - Approve an agent on the desktop by pasting a throwaway testnet main key once. Terminal wipes it after signing and never stores it. In the browser the wallet signs instead (EIP-1193). The agent key lives in the macOS Keychain or the Linux Secret Service, and in browser memory or, by opt-in, IndexedDB under a non-extractable WebCrypto key. There is no file fallback. - The ticket's Hyperliquid variant covers market (IOC, 1% cap), limit (GTC, IOC, post-only), stop loss and take profit, reduce-only and "Set 3x" leverage. HIP-3 rows read "PERP · HL xyz (perpetual, not a share)". - Every order, cancel, cancel-all and leverage change asks for one confirmation. The worker re-reads the testnet mid and leverage and checks the caps again right before it signs. - KILL and a button in ORD cancel every open order and turn Hyperliquid orders off. The dead man's switch is opt-in. - Testnet rows price only from testnet mids. Position.market now carries the exact key, and live_price picks the map by network. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): mask addresses in Hyperliquid messages, toasts and logs Hyperliquid's errors name full addresses ("User or API Wallet 0x… does not exist", "Must deposit… User: 0x…"). mask_addresses turns each one into 0x1234…abcd. Exchange summaries, the worker's logs and every app message that shows a Hyperliquid error now go through it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep Hyperliquid out of SnapTrade's settings sections The offscreen render showed the Hyperliquid account under SnapTrade's Brokerage access, with SnapTrade's "reconnect with trading access" advice. That section and the default-account picker now list SnapTrade accounts only. Hyperliquid has its own section. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(hl-trading): record the v1 decisions and add screenshots and a wasm CI check Section 15 of the design now records Micky's twelve decisions as "Decided (2026-11-02)". The sections they change are updated: testnet only, Secret Service with no file fallback on Linux, a pasted throwaway key on the desktop for now, hard caps, an opt-in dead man's switch and the kill switch. A new section 16 lists what v1 builds and what is still missing. docs/img/hl-*.png holds the before (origin/master) and after screenshots. CI now builds and lints tr-hl for wasm32. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): read the agent key from the store once per account When the store had no key for the approved agent, the app asked again on every frame, which meant a Keychain or Secret Service read per frame. It now remembers the account it read and asks again only after a connect, an approval or a revoke. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): fix the kill switch, uncertain sends, approvals and cap valuation Fixes from Greptile's first review of #37: - Totals value a margined position (a perp) by its P&L, not its notional. Hyperliquid's cash is its collateral (equity less unrealised P&L), so a reprice moves equity only by the P&L change. - KILL reads the open orders fresh on every dex, cancels them, and reads again (three rounds at most) before it reports what is left. Signed actions now run one at a time, and an order confirmed before a KILL that hasn't been signed yet is refused. - An approval answer applies only to the account that asked for it, and Remove is disabled while one is in flight. The app also never signs with a key that isn't the approved agent's. - An uncertain send blocks new orders until orderStatus finds its cloid, or until its 30 s expiresAfter window has passed without it. - Every order needs a current mid before it is valued against the $100 cap. Prices that round to zero are refused, and rounding zero no longer overflows. - The keyring check runs in the worker, not on the UI thread. - A cancel-all skips a market the registry doesn't know instead of failing, and the pasted key field's undo history is wiped once the key has signed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep the dead man's switch armed while an order is unresolved From Greptile's second review of #37: an order with no clear answer blocked new sends through the same guard the dead man's switch reads, so the switch cleared its scheduled cancel just when a possibly accepted order needed it. The guard is now split. Whether this device can sign arms the switch, and allows cancels and leverage changes. Only new orders and modifies also wait for an unresolved order. KILL always goes ahead. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): size dollar amounts at the mid and let KILL skip stalled requests Fixes from Greptile's fourth review of #37 (atf636b7c): - A dollar amount on the Hyperliquid ticket (BUY xyz:INTC $10) became that many contracts. The ticket now sizes it at the testnet mid, rounded down to the market's size step. With no mid yet, or under one step, it stays a dollar amount and the ticket can't send it. Changing the market sizes the amount again. - Every /info and /exchange request has its own 10 s deadline. The browser's fetch had none, so a stalled request could hold the worker's only turn forever. - KILL no longer waits for that turn. It cancels at once, and an order confirmed before it gives up, even one still reading the market. If an order was already being sent, KILL cancels again once its answer comes, or once its expiresAfter has passed if the send got no clear answer. Single cancels and the dead man's switch don't wait for orders either. Orders, modifies and leverage changes still go one at a time, and an order with no clear answer still blocks new ones until it is found or expires. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): size dollars for the market named now and keep timed cancels in order Fixes from Greptile's review off1ff1b3: - The ticket reads its market at the start of a frame, before an edit to the market field. A dollar amount could then be sized at the old market's mid ($10 at a $2 mid is 5 units, worth $20 on a $4 market). settle_size now sizes only with the market the ticket names, so the next frame sizes it for the new one. The ticket's estimate also uses that market. - Dead man's switch changes ran side by side, so a slow clear could land after a newer schedule and remove it. They now go out one at a time in the order the app made them, on a lock of their own that never waits for orders. A change that reaches the worker after a newer one is dropped. After a change with no clear answer, the next waits until it can no longer land. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): give each dead man's switch refresh a fresh deadline From Greptile's review of2f7cab6: after a change with no clear answer, every later change waited until that one could no longer land. Refreshes queue every 30 s, so with lost replies they went out every 35 s or so, each with the deadline it got when queued, and the timer could fire while Terminal was still running. - ScheduleCancel now carries how far ahead to cancel. The worker adds it to the time of signing, so a refresh that waited still gets a fresh deadline. - A newer change replaces one still waiting for its turn, so a backlog collapses into the newest. - Only a change of the other kind waits out a lost one: a timer after a clear, or a clear after a timer. A refresh that lands late only moves the timer back by one refresh, so refreshes don't wait for each other. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): make every dead man's switch change wait out a lost one From Greptile's review of73b71b8: letting a refresh go out before an earlier timer with a lost reply could no longer land was not safe. That timer could land last (the clocks may differ by seconds) and pull the deadline in, so the switch could fire while Terminal was running. Every change now waits until a lost earlier change can no longer land, whichever kind either one is. That is safe now that each change is signed, with its deadline, when it goes out, and a newer change replaces any still waiting, so a backlog can't build up stale deadlines. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(hl): sign approveAgent through a built-in Turnkey wallet, testnet only Add tr-turnkey, the client side of Terminal's built-in wallets. The device makes a P-256 key, and every Turnkey request carries a stamp made with it. The session token Turnkey issues for that key lasts 15 minutes, and Terminal accepts it only if it is bound to its own key. The client reads whoami and the wallet's Ethereum account, and signs one digest with the request alloy-signer-turnkey sends (SIGN_RAW_PAYLOAD_V2, hex payload, no hashing). On the desktop the browser returns the session to a listener on 127.0.0.1 that checks a 256-bit state. A mock feature adds a local stand-in for Turnkey's API and sign-in page for tests and screenshots. tr-hl gets sign_approve_agent_with, the only way a remote wallet's signature enters the crate. The signer sees only the digest of a testnet approveAgent, and the answer is used only if it recovers the wallet. approve_agent_hash now also refuses every signatureChainId except 0x66eee. The mock wallet holds the Python SDK's public test key and reproduces the SDK's approveAgent vector. Mainnet and 0xa4b1 approvals never reach the wallet, and the new code names no mainnet host. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(hl): design built-in Turnkey wallets for testnet trading Record Micky's decision of 2026-11-04 in §15: built-in wallets only, no browser wallet and no pasted key. The new §4.5 covers who holds which key, the browser sign-in that hands a session to a key made on the device, why a loopback redirect beats a terminal:// link, what Turnkey signs and how often, and the export through Turnkey's iframe. It explains why no Terminal server needs a Turnkey key (the Auth Proxy holds the one that creates sub-organizations) and sketches a scoped signup function as an unbuilt fallback. Agents now last 180 days by default, since each approval is a paid Turnkey signature. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(hl): replace the browser wallet and pasted key with the built-in wallet Settings › Trading › Hyperliquid now starts with "Create Wallet / Sign In". On the desktop it opens Terminal's sign-in page in the system browser and waits on a 127.0.0.1 loopback for the session; the web build opens the page in a popup and takes its postMessage. Terminal reads the wallet's address from Turnkey with requests stamped by its own key, keeps the 15-minute session in the OS secret store (memory on the web) and deletes it once it expires. "Approve Trading Key" makes the agent key on the device and has the wallet sign approveAgent through Turnkey, signing in first if there is no session. Revoke goes the same way. Agents now last 180 days by default, since each approval is a paid Turnkey signature. Orders are still signed only by the agent key. Deposit shows the address in full with a QR code and a copy button, the testnet perp account value and withdrawable from the stream, and spot USDC (new spotClearinghouseState read). Back Up Wallet warns, then opens the page's export mode. The web's EIP-1193 Connect Wallet and the desktop's pasted testnet key are gone. MainKey is left for tests and the testnet_check example. Watching an address read-only stays. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(site): add the wallet sign-in and backup page /wallet/ signs the user in with an email code or a passkey through @turnkey/core and Turnkey's Auth Proxy. A new user gets a sub-organization with one Ethereum wallet. The page then calls stamp_login for the app's public key, so the app gets a 15-minute session only its own key can use, and logs itself out. Desktop answers go to the loopback port, web answers to the opener with postMessage on the same origin. Its parameters come from the URL fragment, which never reaches a server, analytics or a Referer, and the page clears it on load. In export mode the page shows the recovery phrase in Turnkey's export iframe, so the phrase never touches the page's code or any server. It needs two public IDs in the Vercel project env and says so until they are set. @turnkey/core is pinned to 2.13.1 and @turnkey/iframe-stamper to 2.11.1. Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci: build and lint tr-turnkey for wasm32 The browser build uses the wallet crate too, so check it the way tr-hl is checked. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep the trading key when the wallet doesn't sign a revoke Revoke used to forget the agent before Turnkey signed anything, so a failed or expired sign-in left the app without its agent while the message said nothing changed. Orders now turn off at once, and the agent is forgotten only when the revoke goes through. An approval the wallet didn't sign now says so instead of reading as Hyperliquid's refusal. The mock-URL warnings no longer print the URL. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): link the sign-in page at /wallet, its canonical URL The site serves the page at /wallet and redirects /wallet/ there. Browsers keep the fragment across that redirect, but there is no reason to rely on it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(site): only sign in, never sign up, when backing up a wallet Export mode hid nothing: it offered "Create with a passkey", and the email code path would create a new wallet for an unknown address. Backing up now logs in to an existing wallet only. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: add before and after screenshots for built-in wallets Desktop shots are offscreen renders of the running app against a local Turnkey stand-in. Web shots come from headless Chromium on a local build. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep sign-in answers and wallet actions on the right wallet From Greptile on #38: - Removing the wallet reset the request counter, so a cancelled sign-in's late answer could carry the next sign-in's id and end it. Ids now keep counting, and removing the wallet cancels a sign-in in progress. - A revoke or approval that needed a sign-in acted on whichever wallet the user signed in to. The sign-in now remembers the wallet the action is for, and refuses another one without changing anything. - The browser build's sign-in never timed out. Both builds now give up after 10 minutes, the limit the waiting line shows. - The sign-in page stayed on "Connecting your wallet" if Turnkey didn't issue the device's session. It now goes back to the sign-in controls with the error, so the user can retry or cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Terminal
A fast, keyboard-driven market terminal in pure Rust: TradingView-class GPU charting with a
Bloomberg Terminal look (dense tiled panes, monospace numbers, a command line like AAPL <GO>)
across stocks, ETFs, indices, FX, crypto, US Treasuries, futures/commodities and options,
plus congressional trade tracking ingested directly from the official STOCK Act disclosure
systems and your own brokerage accounts through SnapTrade
(read-only by default, with opt-in order entry). Native compact UI, themes from Bloomberg amber
to Solarized, and a fully dockable
workspace with saved layouts.
Built from reverse-engineering research. Chart UX was studied behaviourally on tradingview.com with REA in website mode (passive captures of the public, logged-out chart page), then given a Bloomberg Terminal look. Findings, evidence IDs and their design implications are in
docs/analysis/. Only behaviour was studied: no TradingView or Bloomberg code, scripts, icons, fonts or other assets are used or included here. The disclosure parsers were likewise built by examining the public Senate eFD and House Clerk sites and their filings.
Screenshots
Screenshots are from the Linux build under Xvfb with live data (October 2026); on macOS the same UI gets the native title bar with traffic lights, window vibrancy behind the sidebar and SF fonts.
Features
-
Multi-asset with Bloomberg-style market keys —
AAPL <EQUITY>,SPY <EQUITY>,SPX <INDEX>,BTCUSD <CRNCY>,EURUSD <CRNCY>,US10Y <GOVT>,CL1 <CMDTY>(keys display asAAPL US Equity,BTCUSD Curncy,US10Y Govt,CL1 Comdty). The yellow key is optional for known tickers. A built-in catalog covers ~120 instruments (regional indices, equity index futures, energy/metals/grains, the full Treasury curve, majors and crosses, top crypto, popular ETFs and large caps); anything else resolves by convention. -
Command line and search —
<SECURITY> [<FUNCTION>] <GO>(Enter is<GO>); typing anywhere starts a command, with a live interpretation underneath (5→ "Interval 5 minutes") and symbol matches across all asset classes (catalog instantly, plus Yahoo search after a short pause); ↑/↓, Tab, Enter; history with ↑/↓.SECF gold cmdtyopens a full search panel. -
Charts — GPU-rendered candles, OHLC bars, line and area; 1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W, 1M plus quick ranges (1D … ALL); volume; 151 indicators;
CMP <SYM>compare; log scale; crosshair with OHLC legend; smooth pan and zoom over a million bars. Live crypto bars from the Binance websocket (Coinbase fallback). -
Indicators — 151 built-in studies: 23 moving averages (SMA, EMA, WMA, Hull, DEMA/TEMA, KAMA, ALMA, T3, McGinley, VIDYA, FRAMA, MAMA/FAMA, Ehlers filters, GMMA…), 8 bands and channels (Bollinger, Keltner, Donchian, STARC, standard-error and VWAP bands…), 43 momentum oscillators (RSI, MACD, Stochastic, StochRSI, CCI, Williams %R, TSI, KST, Fisher, Schaff, QQE, WaveTrend, Connors RSI…), 22 trend tools (ADX/DMI, Aroon, Vortex, Parabolic SAR, SuperTrend, Ichimoku, Alligator, ZigZag, fractals, Chandelier and Chande Kroll stops…), 16 volatility measures (ATR, close-to-close and Parkinson, Garman-Klass, Rogers-Satchell and Yang-Zhang estimators, Ulcer, Mass index, squeeze momentum…), 23 volume studies (OBV, A/D, CMF, MFI, Klinger, NVI/PVI, VZO…), 11 rolling statistics and five pivot-point families. Type a code with optional parameters (
RSI,SMA 200,BB 20 2.5), browse and search withIND, or right-click the chart. Oscillators get their own panes (up to six) with reference levels; moving averages can be added several times; indicators are saved with the layout. -
Drawings — trendline (Alt+T), horizontal (Alt+H), vertical (Alt+V) and fib retracement (Alt+F), stored in (time, price) so they survive zoom and timeframe changes; magnet (Alt+M) snaps to OHLC; click to select, Del to remove.
-
Options (
AAPL OMON, ⌘⇧O) — full chains from Yahoo for US equities, ETFs and indices (SPX via ^SPX): every expiration, calls and puts by strike with bid/ask/last, change, volume, open interest and IV, plus Black-Scholes greeks (Δ, Γ, Θ/day, vega) from each contract's IV, the dividend yield and the 3-month T-bill rate; in-the-money shading, spot line, ±10/±20/all strikes; offline mode shows a flagged simulated chain. -
Markets —
WEIworld markets (Americas/EMEA/Asia-Pacific indices, futures, Treasuries, FX, crypto and commodities with sparklines),GCUS Treasury yield curve (now vs 1M and 1Y ago, 2s10s / 3m10y / 5s30s), news (AAPL CN; Yahoo headlines, CoinDesk for crypto) and order book depth (BTCUSD CRNCY OB; live Binance book, Coinbase fallback; markets without a free public book show a clearly marked simulation). -
Market data behind a trait (
tr_data::MarketData), routed per asset class to free sources:Asset class History Quotes Live Crypto Binance public REST ( data-api.binance.vision, thenapi.binance.com), Coinbase Exchange fallbackBinance 24h ticker Binance kline websocket, Coinbase ticker fallback US Treasuries Treasury.gov daily par yield curve CSV (FRED fallback); Yahoo ^IRX/^FVX/^TNX/^TYXintradayTreasury.gov — FX, indices, futures Yahoo Finance ( EURUSD=X,^GSPC,CL=F)Yahoo spark (batched) — Stocks, ETFs Alpaca when APCA_API_KEY_ID/APCA_API_SECRET_KEYare set, else YahooYahoo spark — Hyperliquid HIP-3 perps ( xyz:AAPL,xyz:GOLD,xyz:EUR)The Terminal data server only ( TR_SERVER_URL)Server mids Server candles, book and trades A HIP-3 perp is its own instrument, labelled
PERP · HL xyzeverywhere.AAPL US Equitystays the listed stock.AAPL PERPopens the perp linked to it, andPERPor ⇧P switches back and forth. A perp has no other source, so it never gets sample data; without the server it shows why.No keys are required.
TR_PROVIDER=sampleruns fully offline on deterministic, class-aware sample data (BTCUSD near 65,000, EURUSD near 1.09, US10Y near 4.2%) with simulated live ticks, and the live router falls back to sample data when a source is unreachable. Bars are cached in SQLite so charts paint from disk on start. Older history pages in as you scroll left. -
Congress trades (key feature)
- Ingests Periodic Transaction Reports from the Senate eFD (efdsearch.senate.gov: agreement
- CSRF session, DataTables search API, HTML transaction tables) and the House Clerk
(disclosures-clerk.house.gov: yearly
{year}FD.zipXML index, PTR PDFs parsed with pure-Rust text extraction). Scanned paper filings are detected and recorded as such.
- CSRF session, DataTables search API, HTML transaction tables) and the House Clerk
(disclosures-clerk.house.gov: yearly
- Normalises politician, party, chamber, state, ticker, buy/sell, amount range, transaction date
and filed date into SQLite. Party comes from the official rosters (clerk.house.gov
MemberData.xml, senate.govsenators_cfm.xml); politicians are keyed by bioguide ID, which also repairs typos in the source index. - Refreshed on a schedule (every 30 minutes by default,
TR_CONGRESS_REFRESH_MIN,0disables;REFRESH/F6 forces one), incrementally — only unseen filings are fetched. - Panels: latest trades feed with party/chamber/side filters (
CONG); per-ticker "who in Congress traded it" (NVDA CONG, or right-click any ticker); politician profile with trade history and estimated performance (amount-weighted directional return, hit rate, average disclosure lag) (POL PELOSI); directory (POL). - Chart overlay: B/S markers at transaction dates coloured by party (blue D, red R, purple I); hover for a card with politician, amount, owner, traded/filed dates and the disclosure lag.
- Alerts:
TRACK <name>(or ☆ in the directory/profile); when a tracked politician files a new PTR you get a toast, anALERTS nbadge and an entry inALERTS.
- Ingests Periodic Transaction Reports from the Senate eFD (efdsearch.senate.gov: agreement
-
Dockable workspace and saved layouts
- Any panel (chart, watchlist, news, order book, world markets, yield curve, options, Congress
feed, politicians, alerts, search, settings, help) docks anywhere: drag a tab (or a pane's
header) onto another pane and drop on the compass (centre adds a tab, arrows split), with a
ghost preview and a labelled landing highlight, or drag to a window edge. Resize with
dividers (double-click to equalise), tabs in every pane, grid presets (
LAYOUT 1|2|2H|3|4|6or the toolbar), and pop any pane out into its own native window (close it to dock back). Add panels from each pane's + menu,NEW <PANEL>/SPLIT [DOWN] <PANEL>or ⌘1…⌘0. - Link groups: give panes a colour (
LINK BLUE, or the tab menu) and one security change drives every linked chart, order book, news, options and per-ticker Congress pane; clicking a row in a linked watchlist or world-markets pane loads it into the group. - Saved layouts: save, name, rename, duplicate, delete and switch (
LAYOUT <name>, the sidebar list, ⌥1…⌥9); the active layout autosaves and is restored on launch. Each layout is a JSON file in~/Library/Application Support/terminal/layouts($XDG_DATA_HOMEon Linux) and can be imported or exported (LAYOUT EXPORT/IMPORT, or Settings › Layouts). Per-pane settings (chart type, indicators, drawings, filters, depth, link, tab title) and, optionally, the theme (LAYOUT THEME ON) save with the layout.
- Any panel (chart, watchlist, news, order book, world markets, yield curve, options, Congress
feed, politicians, alerts, search, settings, help) docks anywhere: drag a tab (or a pane's
header) onto another pane and drop on the compass (centre adds a tab, arrows split), with a
ghost preview and a labelled landing highlight, or drag to a window edge. Resize with
dividers (double-click to equalise), tabs in every pane, grid presets (
-
Themes — built-in Terminal Dark, Terminal Light, Bloomberg (amber on black, monospace, compact), High Contrast, Solarized Dark/Light, Nord and Monokai. A theme sets every colour (backgrounds, panels, text, accent, up/down, candles, grid, series), the interface and number fonts and size, and density (compact, regular, comfortable). Pick one from the gallery in Settings › Appearance, the View menu,
THEME <name>(prefix match),THEME NEXT|PREV|LISTor ⌘⌥T;THEME SYSTEMfollows the OS with a chosen dark and light theme. Settings › Themes is a live editor: editing a built-in makes a copy (built-ins stay read-only); duplicate, rename, delete, import or export as TOML or JSON. Custom themes are TOML files in thethemesfolder next tolayouts; a partial file inherits everything else from Terminal Dark or Light. -
Native, compact UI — a macOS unified compact toolbar with the traffic lights inset, a translucent source-list sidebar with window vibrancy (
NSVisualEffectViewviaobjc2, incrates/macos) that can sit on either side (SIDEBAR LEFT|RIGHT, ⌘⌥S; the toolbar toggle moves and mirrors with it, right-click it for the View menu), SF Pro for chrome and SF Mono for numbers, segmented controls, spring animations, hover states and context menus. The System theme follows the OS appearance and accent colour; charts render at native retina scale with 4x MSAA. -
Brokerage accounts (SnapTrade) — connect your brokers once through SnapTrade's Connection Portal (opened in your browser) and the terminal shows real accounts and balances (
ACCT), positions with unrealised P&L (POS), orders from the last 30 days (ORD), activity such as trades, dividends, interest and transfers (ACTV) and a portfolio summary with allocation (PORT). Holdings are repriced with the terminal's own live quotes; clicking a position, order or activity row charts it, and right-click offers the option chain, news, Congress trades and watchlist.AAPL POS/AAPL ORDfilter to one security, the account picker to one account. Stocks, ETFs, funds, crypto and options (OCC symbols shown asAAPL 350C) are supported. Read-only unless you turn on trading (below); Connect Brokerage requests read-only access. Without credentials the panes show a setup screen; offline (TR_PROVIDER=sampleorTR_BROKER=sample) they show a clearly labelled simulated portfolio. See Brokerage setup. -
Demo account and easy trading — a DEMO paper-trading account ($100,000 virtual cash) is selected by default; the toolbar badge shows DEMO or a red LIVE and switches between them (switching to live asks first and is remembered). Buy/Sell buttons sit on charts, watchlist rows and positions (plus Close), ⇧B / ⇧S trade the security in focus, and
BUY AAPL 10,SELL ALL TSLA,BUY NVDA $500,SELL MSFT 5 LMT 410work from the command line or ⌘K. One compact ticket: side, shares or dollars, market/limit (stops under More), live estimated cost against buying power. Demo orders fill in one click at the live quote (limits and stops when the quote crosses) and persist in the app data folder; Settings › Trading resets the account. Live orders stay opt-in and go through SnapTrade's preview and a single confirmation. See Trading. -
Command palette — ⌘K (Ctrl+K on Linux and Windows) opens a centred, theme-matched palette that fuzzy-searches every function code (
PORT,POS,ORD,OMON,THEME, …), tickers (load the quote and chart), new panels, layout presets and saved layouts, themes, Settings tabs and workspace actions (move the sidebar, open Brokerage settings). Each row shows its shortcut; recent picks come first. ↑↓ select, ↵ open, Esc close. -
Hotkeys and preferences — every hotkey is rebindable from a native shortcut table in Settings › Hotkeys and Help (click a binding, press the new keys); defaults for new charts and order books; everything persists in
prefs.json.
Running
cargo run --release # Yahoo data, congress ingest in the background
TR_PROVIDER=sample cargo run --release # offline
APCA_API_KEY_ID=… APCA_API_SECRET_KEY=… cargo run --release # Alpaca
Data lives in ~/Library/Application Support/terminal (macOS) or
$XDG_DATA_HOME/terminal (Linux), or TR_DATA_DIR. Versions from before the project was renamed
used a terminal-rs folder in the same place; the first launch moves it to terminal and leaves
a link at the old path, so builds that still use the old name find the same files. The first
congress ingest covers the current and previous year (a few minutes; it runs in the background
while you work).
cargo run -p tr-congress --example ingest -- congress.sqlite3 runs one ingest from the CLI.
Appearance: TR_THEME=light|dark|<theme name> overrides the saved theme, TR_MSAA=1|2|4|8 sets
chart anti-aliasing (default 4 on macOS), TR_VIBRANCY=0 disables the translucent sidebar, and
⌃⌘S (Ctrl+Shift+S elsewhere) toggles the sidebar. scripts/bundle-macos.sh builds a retina-capable
Terminal.app (icon from scripts/build-icon.sh); scripts/release-macos.sh builds the universal (Apple Silicon + Intel) app,
signs it with a Developer ID certificate, notarizes it and packages Terminal.dmg,
plus the zip and signed appcast.xml that installed copies update from (one-time credential
setup is at the top of the script). The app checks that feed once a day through
Sparkle and offers to install a newer release; Terminal › Check
for Updates… or UPDATE checks at once. scripts/check-macos-cross.sh compiles
and lints the whole workspace for aarch64-apple-darwin from Linux (zig as the C cross-compiler;
linking needs the macOS SDK).
In the browser
The same code also builds for WebAssembly and runs in a browser tab on WebGPU:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.129 --locked # must match Cargo.lock
brew install binaryen # wasm-opt
scripts/build-web.sh # writes site/static/app
cd site && npm run dev # then open http://localhost:5173/app/index.html
scripts/build-web.sh <dir> writes somewhere else instead; any static file server works. Add
?sample to the URL to run offline on sample data, or ?fps for the frame-time readout.
Every push to master builds it and deploys it with the site to
terminal.rasmic.xyz/app. Pull requests get a preview;
site/README.md covers the workflow and the secrets it needs.
What the browser build can and can't do yet:
- Crypto is live, because Binance and Coinbase allow cross-origin requests. History, quotes, order books and websocket bars all come from them. Yahoo, Treasury.gov, Alpaca and CoinDesk don't allow them, so every other market shows sample data and the toolbar says "Crypto live". A same-origin proxy can fix that later.
- HIP-3 perps (
xyz:AAPL) are live through the Terminal data server the page is built with. They never fall back to sample data: while the server is unreachable they say so. - Congress disclosures, SnapTrade accounts and live order entry need the desktop app. The paper trading demo account works.
- Layouts, settings, themes and the demo account are saved in
localStorageunderterminal/. The first load copies anything saved under the oldterminal-rs/prefix, without overwriting newer entries. - Charts need WebGPU, since they read bars from storage buffers in the vertex shader. Browsers without a WebGPU adapter get a second, larger build on WebGL2 where everything except the chart series works.
Measured with Playwright driving Chrome 155 on an Apple M4 Pro:
| WebGPU build | WebGL2 build | |
|---|---|---|
.wasm |
5.60 MB (2.26 MB gzip, 1.69 MB brotli) | 7.48 MB (2.99 MB gzip, 2.22 MB brotli) |
| JS glue | 115 KB | 169 KB |
- A cold load from localhost draws its first frame 91 to 97 ms after navigation, the medians of three runs of 9 loads, two headless and one in a Retina window. On a 50 Mbit/s link with 40 ms of latency it takes 446 ms, and 1.64 s on 10 Mbit/s with 80 ms.
- Panning and zooming a chart costs 1.3 to 1.5 ms of CPU in the UI code and 2.4 to 3.3 ms for the whole frame (medians across runs), and holds the display's 60 Hz.
- egui's two emoji fonts, 740 KB together, load after the first frame instead of inside the
.wasm.
Brokerage setup (SnapTrade)
Everything happens in Settings › Brokerage, with no commands needed. Open it from the sidebar
(Portfolio › Brokerage Settings), the toolbar's View menu (Brokerage Accounts…), the gear
button on any portfolio pane, or the setup screen; PORT SETUP still works too.
- Create API keys (a client ID and consumer key) in the SnapTrade dashboard. Personal keys work too.
- Enter the Client ID and Consumer Key (masked; Show secrets reveals them) and click Save.
- Click Register User. This creates a SnapTrade user for this installation and saves its user secret, which SnapTrade only returns once. With personal keys, leave the user fields empty.
- Click Connect Brokerage…. SnapTrade's Connection Portal opens in your browser with read-only access; sign in to your broker there. New accounts appear within a minute.
The status card at the top always says where you are and what to do next ("Not set up", "Connected · 3 accounts at 2 brokerages · synced 14:32", or the API's error). Test Connection reloads accounts and connections on demand. Connected brokerages lists every connection with its accounts; a connection the broker has expired shows Reconnect…, which re-authenticates it in the portal, and Disconnect removes a connection and its accounts from SnapTrade after an explicit confirmation.
Where credentials live. On macOS the client ID, consumer key, user ID and user secret are
stored together in the Keychain (generic password, service terminal, account
snaptrade); credentials saved under the old service, terminal-rs, move there the first time
the app reads them. Elsewhere they go in <data dir>/secrets/snaptrade.json with owner-only permissions
(0600 in a 0700 directory). They are never written to the repository, layouts, prefs or logs;
request errors are scrubbed of the URL (which carries the user secret), and the settings screen
masks secrets. Forget Credentials removes them. SNAPTRADE_CLIENT_ID,
SNAPTRADE_CONSUMER_KEY, SNAPTRADE_USER_ID and SNAPTRADE_USER_SECRET override the saved
values field by field (useful for CI or a shell profile), and TR_SECRET_STORE=file uses the
file store on macOS.
How it talks to SnapTrade. Requests go to https://api.snaptrade.com and are signed as the
SnapTrade docs specify: clientId and a Unix timestamp in the query, and a Signature header
holding a base64 HMAC-SHA256 (keyed with the consumer key) of the canonical JSON
{"content","path","query"}. Portfolios refresh every two minutes while a portfolio pane is
visible (every 20 s for five minutes after opening the portal); PORT REFRESH reloads on demand.
Brokerage data can be delayed or cached by SnapTrade depending on your plan, which is why holdings
are repriced with the terminal's live quotes where a market key exists.
Trading (opt-in)
The demo account needs no setup: it is a local paper-trading account (demo-account.json in
the app data folder) with no commissions, no short selling and Day orders expiring at the 4 p.m.
New York close. Switch to your brokerage accounts with the toolbar badge, MODE LIVE, or
Settings › Trading (DEMO switches back).
Live trading is off until you enable it in Settings › Trading, which asks you to acknowledge that confirmed orders are real. Then:
- Make sure the connection allows trading. Connections made with Connect Brokerage are
read-only; Reconnect with Trading Access… in Settings › Trading opens the Connection Portal
asking for trading access where the broker supports it (
trade-if-available). Settings › Trading lists, per brokerage, whether trading is available, from SnapTrade'sallows_tradingflag and each connection's access type. - Open a ticket (
BUY AAPL 10, a Buy/Sell button, ⇧B / ⇧S), pick the account and order type, and click Review. The broker checks the order through SnapTrade's order impact endpoint; nothing is placed. - The broker's estimate opens one confirmation dialog. Send places exactly the previewed trade
(
POST /trade/{tradeId}). Editing the ticket after previewing, or waiting past the preview's validity (SnapTrade expires it after five minutes), requires a new preview. A preview is used up when it is sent. If the answer is lost (network error, timeout, 5xx), the ticket reports the status as unknown and opensORD, because the broker may have accepted the order; only a 4xx is reported as a rejection.
Which order types a broker accepts varies and SnapTrade does not publish it per broker, so the preview is where unsupported combinations are rejected. Dollar amounts work only for market/day orders at brokers that support notional orders; fractional share quantities are refused up front at brokers SnapTrade reports as not supporting them. Settings › Trading also has an optional maximum order value and a default account.
Trading access is read once per brokerage connection (/authorizations), cached for an hour (on
disk too) with a Refresh button. SnapTrade requests are limited to four at a time; a
rate-limited read waits for Retry-After and retries, and a rate limit is shown as a quiet
"retrying" note rather than an error. Orders are never retried automatically.
Tests never place real orders: the trading client is exercised only against a local mock server, and the app's flows against the simulated portfolio and the demo account.
Keyboard shortcuts and commands
Press F1 (or type HELP) for the full, searchable list; every hotkey can be rebound in
Settings › Hotkeys. On Linux and Windows ⌘ is Ctrl and ⌥ is Alt.
| Keys | Action |
|---|---|
| ⌘K (Ctrl+K) | command palette: fuzzy search over commands, tickers, panels, layouts, themes, settings and actions |
| F1 · F2 · F3 · F4 · F5 · F6 · F12 | help · Congress feed · politicians · alerts · watchlist · refresh filings · frame timing |
| F7 · F8 · F9 | brokerage portfolio · positions · orders |
| Tab · ⇧Tab | next · previous pane |
| ⌘1 … ⌘0 · ⌘⇧O | new chart, watchlist, news, order book, world markets, yield curve, Congress, politicians, alerts, search pane · options chain |
| ⌘D · ⌘⇧D · ⌘T · ⌘W · ⌘⇧P | split right · split down · new tab · close pane · pop out |
| ⌘S · ⌥1 … ⌥9 | save layout · switch to saved layout 1–9 |
| ⌃⌘S (Ctrl+Shift+S) · ⌘⌥S | toggle sidebar · move sidebar to the other side |
| ⌘⌥T · ⌘, | cycle theme · settings |
| ⌥T · ⌥H · ⌥V · ⌥F · ⌥M | draw trendline · horizontal · vertical · fib · magnet |
| ⌥R · ⌥L · Esc · Del | reset chart view · log scale · cancel · delete selected drawing |
| wheel · drag · drag axes · double-click | zoom · pan · stretch · reset |
| Command | Action |
|---|---|
AAPL · AAPL US EQUITY <GO> |
load a security in the focused chart |
BTCUSD <CRNCY> · EURUSD <CRNCY> · US10Y <GOVT> · CL1 <CMDTY> · SPX <INDEX> |
other asset classes |
5 1H D 1W 1M · YTD 5Y ALL |
interval · quick range |
RSI MACD SMA 200 BB 20 2.5 · IND IND VOLUME · CLEAR |
toggle any of the 151 indicators (optional parameters) · indicator picker · remove all |
CMP MSFT · LOG · CANDLE BAR LINE AREA |
compare · log scale · chart type |
AAPL OMON · AAPL CN · BTCUSD CRNCY OB · WEI · GC · SECF <query> |
options · news · order book · world markets · yield curve · search |
CONG · NVDA CONG · POL PELOSI · TRACK PELOSI · ALERTS · REFRESH |
Congress feed · who traded it · profile · track · alerts · refresh |
NEW BOOK · SPLIT DOWN NEWS · POP · LINK GREEN · TITLE Desk · CLOSE |
add, split, pop out, link, rename, close panes |
LAYOUT 4 · LAYOUT SAVE Macro · LAYOUT Macro · LAYOUT RENAME Rates · LAYOUT DUP · LAYOUT DEL · LAYOUT EXPORT |
grid preset · saved layouts |
THEME BLOOMBERG · THEME NEXT · THEME SYSTEM · THEME EDIT · THEME COMPACT · LAYOUT THEME ON |
themes |
PORT · ACCT · POS · ORD · ACTV · AAPL POS · AAPL ORD |
brokerage portfolio · accounts and balances · positions · orders · activity · one security |
PORT REFRESH · PORT CONNECT · PORT SETUP (BROKER) |
reload accounts · connect a broker in the browser · SnapTrade keys |
BUY AAPL 10 · SELL ALL TSLA · SELL MSFT 5 LMT 410 · SELL TSLA 3 STP 250 LMT 245 · BUY NVDA $500 · TRADE |
order ticket, prefilled (demo: one click; live: review and confirm) |
DEMO · MODE LIVE · ⇧B · ⇧S |
switch account mode · buy / sell the security in focus |
SIDEBAR RIGHT · SIDEBAR · PREFS · HELP |
sidebar side or visibility · settings · help |
| right-click chart / watchlist / feed row / sidebar toggle | context menu (chart type, indicators, draw, add to watchlist, who traded it, View menu) |
Architecture
app/ eframe shell: command line, docked panes, chart widget, panels
dock.rs docking tree (splits + tab groups): insert/move/remove, layout, resize, presets
layouts.rs saved layout documents (JSON) and the layouts folder store
prefs.rs appearance, keymap and panel defaults (prefs.json)
themes.rs theme model, built-in themes, TOML/JSON theme files and the themes folder
app/ workspace (drag and drop, pop-out windows, link groups), panels, settings, chrome
worker.rs tokio runtime: all network, SQLite, pyramid and indicator work (UI never blocks)
storage.rs saved files: the data folder on desktop, localStorage in the browser
web.rs browser entry point (eframe web runner), console logging, frame timings
web/ the page that hosts the browser build
crates/core Bar/Series, timeframes, market keys + security catalog, indicators and their
registry (codes, parameters, panes, levels), sample data
crates/data MarketData trait; per-class router (Binance/Coinbase, Treasury.gov/FRED, Yahoo,
Alpaca), websocket live feeds, sample provider with simulated ticks; bar cache;
the browser router (crypto live, the rest sampled) and a tokio/browser task shim
crates/chart wgpu renderer, LOD pyramids, viewport math, axes, drawings, egui paint callback
crates/congress Senate eFD + House Clerk parsers, rosters, SQLite store, refresh, alerts, performance
crates/broker SnapTrade client (request signing, accounts, balances, positions, orders, activity),
order entry (impact preview, place, cancel, capabilities), portfolio model and P&L,
Keychain / owner-only credential store, simulated sample and order simulator
docs/analysis REA research notes on TradingView chart UX
Rendering. Bars are uploaded once per data version into a storage buffer as a level-of-detail pyramid (level k merges 2^k bars), and geometry is generated in the vertex shader (one instance per bar or line segment). Each frame picks the level that keeps instances at ≥ 2 px, so the work per frame is bounded by the plot width, not the series length: a 1M-bar chart fully zoomed out draws about 800 instances. Pan and zoom only rewrite a few hundred bytes of uniforms; bar indices are split into integer and fraction for f32 precision at millions of bars. Axes, legend, crosshair, drawings and markers are cheap egui shapes.
Threading. The UI thread sends requests to the worker and drains responses each frame; history fetches, pyramid building (≈ 200 ms for 1M bars), indicators, SQLite queries and the congress ingest all run on tokio tasks. The worker owns each chart's bar series, so initial history, paging and live websocket ticks merge into one copy; live updates are coalesced to at most four GPU re-uploads a second.
Performance
Measured on Linux with Mesa llvmpipe (a CPU software rasterizer) — a real GPU is much faster:
-
Cold start: window and GPU ready in 143 ms; charts paint from the SQLite cache as soon as the worker answers.
-
1M-bar pan/zoom (
cargo run --release -p tr-chart --example framebench, 1600×900, candles + volume + 2 overlays, 300 frames per phase):phase CPU per frame p50 / p99 until GPU done p50 / p99 zoom out to all 1M bars 0.07 / 0.20 ms 3.09 / 5.27 ms pan across history 0.06 / 0.18 ms 1.40 / 3.93 ms zoom in to 8 px/bar 0.05 / 0.19 ms 1.34 / 2.63 ms All within the 8.33 ms budget for 120 fps even on the software rasterizer. Criterion versions:
cargo bench -p tr-chart --bench frame. -
In the app (F12 readout, interactive wheel-zoom + drag): UI CPU ≈ 0.2 ms per frame, 118 fps. To try a million bars interactively:
TR_PROVIDER=sample TR_SAMPLE_BARS=1000000 TR_HISTORY_LIMIT=1000000 TR_FPS=1 cargo run --release, then1 <GO>. With all 1M one-minute bars visible on llvmpipe under Xvfb the app measured UI CPU 0.36 ms and 76 fps for the whole window (software rasterization and presentation bound; the chart pass itself is the benchmark above). -
Indicators over 1M bars (
cargo bench -p tr-core): SMA 1.8 ms, EMA 2.2 ms, VWAP 7.0 ms, Bollinger 29 ms; RSI and MACD are single-pass (≈ 12 ms and 26 ms including first-touch of their output memory). The whole 151-indicator catalogue at default parameters over 100k one-minute bars takes ≈ 134 ms on macOS (Ichimoku, the slowest, 6.6 ms).
Tests
cargo test --workspace # unit + fixture tests (parsers, indicators, chart, app)
cargo test -p tr-data -- --ignored # live tests: every asset class, quotes, curve, search, websocket
cargo bench -p tr-core && cargo bench -p tr-chart
- Parsers are tested against real filings captured from the official sources
(
crates/congress/tests/fixtures): multi-page House PDFs with partial sales and page-break rows, spouse/joint owners, wrapped and open-ended amounts, CUSIP vs ticker, exchanges, private holdings, a scanned paper filing, Senate search JSON (electronic, paper and amended reports) and PTR tables. An end-to-end refresh test covers discovery, roster joins, idempotency and alerts. - Data-source parsers are tested against captured responses: Binance klines/24h ticker/websocket kline frames, Coinbase candles/stats/websocket ticker, Treasury.gov curve CSV, FRED CSV, Yahoo chart/spark/search. Market-key parsing, catalog search and symbol mapping per source are unit tested; opt-in live tests hit the real sources for every asset class.
- Indicators are checked against reference values (Wilder's RSI example, hand-computed EMA, MACD, population-SD Bollinger, session VWAP) and hand-computed windows (stochastic, %R, CCI, WMA, true range, Donchian, envelopes, %B, OBV and A/D, CMF, Aroon, pivots). Every catalogue entry runs on intraday and daily random walks and on degenerate input (no bars, one to three bars, flat zero-volume bars) at its default, minimum and maximum parameters, checking alignment, declared ranges and no panics; layouts saved in the old indicator format still load.
- Layouts: the dock tree (insert, split, tab, move, remove/normalise, resize clamping, presets, JSON round trip and repair of hand-edited files), layout documents (every panel kind and its settings, links, popped windows, older formats), the store (save, list, rename, duplicate, delete, import, export, safe file names), migration from pre-docking settings, prefs and keymap parsing/rebinding.
- Themes: every built-in round-trips through TOML and JSON, partial theme files inherit from the dark/light base, the themes folder store (save, rename, delete, name collisions), copy-on-write names and migration of older appearance preferences.
- Brokerage: request signatures match values computed independently with Python's
hmacover the documented canonical JSON; an end-to-end test runs the client against a local mock SnapTrade server that verifies every request's signature, timestamp and auth parameters and serves fixtures shaped like the API's responses (stocks, ETFs, crypto, options, cash equivalents, string and numeric decimals, a failing account); P&L for longs, shorts and option multipliers; market-key mapping; OCC symbols; credential storage permissions and redaction; errors never echo the user secret; every portfolio view renders. - Trading: order validation (prices per order type, stop-limit direction, notional rules,
path-safe ids); a mock SnapTrade trading server checks the signed symbol lookup, order impact,
placement of the previewed trade, cancel and portal requests; expired or simulated previews are
never sent; per-account capabilities from brokerage and connection;
BUY/SELL/TRADEparsing; edited tickets invalidate their preview; the simulator fills, rests and cancels orders. - The chart renders real frames offscreen on whatever adapter is present and checks pixels.
Platform notes and limitations
- macOS is the primary target (wgpu Metal backend); Linux works with Vulkan or GL. This repository
was developed and run on Linux (X11, software Vulkan). For macOS, the GPU and UI stack (
tr-core,tr-chart,tr-macos, eframe/winit) is cross-checked foraarch64-apple-darwin, and CI (.github/workflows/ci.yml) builds, tests, lints and launches the app with screenshots on macOS arm64 and Intel runners. - Scanned paper filings (some House PTRs and Senate
/paper/reports) are recorded and alerted but not OCR'd, so their trades are not itemised. - Estimated performance uses disclosed value bands (midpoints), so it is an estimate of trade-level skill, not a reconstruction of anyone's portfolio.
- Yahoo's chart endpoint is unofficial and rate-limited; use Alpaca for a supported free source. Senate eFD terms prohibit using the data for commercial purposes or credit decisions; the terminal accepts the eFD agreement on your behalf for personal research use.










