# Finterm — full content > Finterm is a free, browser-based, keyboard-first financial terminal — a lightweight alternative to Bloomberg, TradingView and Koyfin. This file inlines the full documentation and blog so an LLM can answer from the whole site in one fetch. See https://finterm.xyz/llms.txt for the link index. # Documentation ## Getting Started ### Introduction Source: https://finterm.xyz/docs/introduction > What Finterm is, what it isn't, and how it's built. Finterm is a financial terminal that runs in your browser. It opens to a workspace of tilable windows — charts, spreadsheets, news feeds, SEC filings, a JS-sandboxed backtester — that you spawn through a single command palette. Workspaces are local-first: everything you do persists to your browser's IndexedDB, and you can export an entire layout as a single .ftm file. Market data flows through one backend gateway that aggressively edge-caches every upstream call, so the chart stays fast and the upstream APIs (Binance, SEC EDGAR, Alpha Vantage, RSS aggregators) never see traffic spikes from the user pool. ### What's wired today - Crypto charts: live OHLCV from Binance with WebSocket tick streams. BitMEX, OKX, and Bitstamp picker entries also exist. - US equities: SEC company search (~10k tickers), filings (10-K / 10-Q / 8-K / DEF 14A), quarterly fundamentals pivoted from the EDGAR companyfacts JSON. - Indicator scripting: drop a JS function into the chart's editor and your output draws as a polyline overlay or scalar readout. - Backtester: same JS sandbox, runs your strategy bar-by-bar against any Binance pair, returns trades + equity + max drawdown. - Workspace persistence: drag, resize, snap, save layouts, share via URL, export to .ftm. ### What's deliberately not here - Order execution. There is no exchange-API-key field, no margin slider, no execute button. Finterm is a watch-and-research tool. - Mobile-app polish. The dashboard auto-tiles on phones (one window full-bleed, swipe between via the tab strip), but desktop is the design target. - Cross-device sync. Workspaces live in your browser; if you clear site data, you lose them. Sync is on the roadmap. > [tip] The fastest way to learn the app is to hit ⌘K (Ctrl+K on Windows / Linux) and start typing. Every action — open windows, switch theme, change a setting, search a ticker — is reachable from the palette. ### Quick start Source: https://finterm.xyz/docs/quick-start > From a fresh tab to a working layout in a minute. ### Sixty-second tour 1. Visit finterm.xyz/dashboard — no account needed. 2. Press ⌘K (or Ctrl+K) to open the command palette. 3. Type a ticker — try BTC, ETH, AAPL, or 'apple'. The palette ranks by symbol and company name. 4. Pick the entry; a window spawns. 5. Repeat. Drag windows by their title bar; resize from any edge. 6. Hit ⌘⇧S to copy a share-URL of the current layout, or use the menu to export .ftm. ### Useful first searches - **BTC** — Spawns a Binance BTCUSDT chart with live ticks. - **AAPL** — Stub equity chart (placeholder while OHLCV is being wired) with company name + SEC links. - **AAPL filings** — Pre-loaded SEC EDGAR filings list. - **AAPL SEC** — Pre-loaded us-gaap quarterly facts. - **theme** — Jumps to the theme picker. Cycle preset, accent hue, density. - **watchlist** — Spawns the watchlist (you can paste a list of tickers). - **news** — Crypto news aggregated from CoinDesk, CoinTelegraph, Decrypt, The Block, Bitcoin Magazine, HN. - **heatmap** — Top USDT pairs by 24h percent change. > [info] There's a fallback in the palette: any 2–15 character alphanum string that doesn't match an existing crypto symbol shows an 'Open chart for X' option that points at XUSDT on Binance — handy for new listings the registry doesn't know about yet. ### Keyboard shortcuts Source: https://finterm.xyz/docs/shortcuts > The full keymap. The palette is everything; the rest is muscle memory. ### Global - **⌘K · Ctrl+K** — Open / close the command palette. - **⌘⇧S · Ctrl+Shift+S** — Copy a share-URL of the current workspace. - **Esc** — Close the palette / dismiss popovers. - **⌘+, · Ctrl+,** — Open the settings JSON (or use the GUI panel via the palette). ### In the palette - **↑ ↓** — Navigate results. - **↵ Enter** — Run the highlighted command. - **Type-to-filter** — Substring match across label, category, and hint. Lowercase 'apple' finds AAPL. ### Inside the chart - **Wheel / two-finger swipe** — Zoom (vertical) or pan (horizontal). - **Click + drag** — Pan with sub-pixel precision. Release with momentum to fling-glide. - **1s / 1m / 1h / 1d** — Timeframe — segmented control in the top bar. - **ƒ Indicators** — Open the presets menu. Click an entry to apply + open in the linked editor. - **⚯ Editor** — Open the linked text editor for the chart's current indicator code. ### Inside the text editor - **⌘T · Ctrl+T** — New tab. - **⌘W · Ctrl+W** — Close current tab. - **⌘S · Ctrl+S** — Mark saved (persistence is automatic — this is just the visual confirmation). - **Tab** — Insert two spaces. ## Workspace ### Window layout Source: https://finterm.xyz/docs/layout > Drag, resize, tile, snap, popout. How the workspace handles multi-window layouts. Each window is freely positioned by default — drag the title bar to move, drag any edge or corner to resize, double-click the title bar to maximize. Snap handles on the edges of the workspace let you halve the screen with a window per side. ### Per-window minimum sizes Windows enforce a minimum size at spawn, resize, and on workspace restore. Below the threshold, internal UI elements collapse — the chart's price pill hides under 620 px wide, buttons go icon-only under 460 px, and the chart can shrink down to 360 × 260 with the candle area still usable. ### Mobile auto-tile On screens narrower than 768 px the workspace switches modes: only the active window renders, full-bleed. Open windows show as pills in a horizontally-scrollable strip at the bottom — tap to switch, × to close, + to open the command palette and spawn a new one. Drag and resize chrome are suppressed since they don't make sense on touch. ### Pop-out Click the ↗ icon in any window's title bar to open it in a new browser window. The popout shares state with the main dashboard — edits in either side propagate live. Close the popout to return the window to the main workspace. > [info] If you resize the top edge of a window and immediately drag its title bar, the position update is consistent — earlier builds had a regression where the top-resize jumped because the drag handler held stale coordinates. Fixed in the smooth-scroll pass. ### Multiple workspaces Source: https://finterm.xyz/docs/workspaces > Layouts as first-class objects — switch between, name, share, export. A workspace is a named collection of windows with their positions. The bottom bar shows tabs for every workspace; click + to add one, double-click to rename, ✕ to close. The active workspace is the one whose tab is highlighted. ### Sharing 1. Build a layout you want to share. 2. Press ⌘⇧S (or use the menu) to copy a share-URL to your clipboard. 3. Anyone who opens the URL gets the same layout — windows, positions, even chart symbols and timeframes — but no data flows from your workspace to theirs. ### Export / import (.ftm) The 'Export .ftm' command writes the active workspace as a single JSON file with the .ftm extension. 'Import .ftm…' loads one back. The file format is stable and self-describing — useful for backups, version control, or sharing layouts that exceed URL length. ### Presets The palette ships preset layouts: Markets, Research, Reading, Watch. Each one builds a workspace tuned for a specific workflow — a chart + watchlist + alerts trio for Markets, a chart + spreadsheet + backtester triptych for Research. ### Files Source: https://finterm.xyz/docs/files > Local-first file storage. Same store backs the FileBrowser, TextEditor, Spreadsheet, and indicator code. Every editor in Finterm reads and writes through a unified IndexedDB-backed store. Open the FileBrowser window to browse what's there; double-click any file to spawn it in the appropriate editor (text → TextEditor, sheet → Spreadsheet, json → TextEditor). ### Auto-save Spreadsheet and TextEditor auto-save with a 4-second debounce after the last keystroke. The status bar shows the dirty / saved state explicitly so you know what's persisted. SettingsPanel's debounce is tighter (1 s) since changes apply immediately. ### What's NOT in this store - Workspace layouts — those live in their own IDB key, distinct from files. - Custom indicators — also a separate IDB key (chart.customIndicators.v1) so they can be enumerated independently. - Settings — stored as a single structured value via lib/settings.ts, not as a regular file. > [warn] Local-first means local-only for now: clearing browser site data wipes all of it. Cross-device sync is on the roadmap; until then, export .ftm regularly if a layout matters. ## Markets ### Charts Source: https://finterm.xyz/docs/charts > WebGL candle renderer with smooth pan / zoom, indicator overlays, and pluggable data providers. The chart is a custom WebGL renderer — candle bodies + wicks in shaders, axes and overlays in a 2D context above. Live ticks come from provider WebSockets where available (Binance, BitMEX) and REST polling otherwise (OKX, Bitstamp). The renderer holds up to 5000 candles in memory and slices a viewport of 8–5000 visible bars. ### Pan & zoom - Mouse drag pans pixel-perfectly via a sub-pixel matrix translation that folds whole-bar shifts into viewStart and renders the residual continuously. - Wheel zoom is RAF-eased: events nudge a target maxPoints, an animation loop interpolates the visible value over ~12 frames. - Trackpad detection (rapid event cadence, fractional dy, small magnitude) automatically switches to slow tunings — about 12× less sensitive than a notched mouse wheel. - Release a drag with momentum and the chart fling-glides for ~600–800 ms before settling. - You can scroll past the latest bar into empty 'future' space (TradingView style) by panning right — useful for trendlines or projections. ### Price-axis scaling - Click + drag the right-edge price axis (last 60 px of the chart) up or down to stretch or compress the y range — same gesture as TradingView. Drag down expands the visible price range (chart compresses); drag up contracts it (chart stretches). - AUTO button (bottom-right of the chart) refits the y range to the visible candles' high/low. - LOG button toggles the y mapping between linear and log10 — bars, indicators, price labels, and the crosshair tooltip all route through the same mapping function so they stay in lock-step. - MAG button (or `m` hotkey) snaps the crosshair to whichever of the bar's open / high / low / close is closest to the cursor — useful before drawing tools land. ### Drawings - Bottom-left palette: H-LINE (single click), TREND (drag), RECT (drag). - Drawings anchor to global bar indices + prices, so they pan / zoom with the chart and survive a workspace reload. - ✕ N button clears every drawing on the chart in one click; the count next to it shows how many shapes are stored on this window. ### Measure (Shift + drag) - Hold Shift, click and drag anywhere on the chart to draw a measurement box. Centre pill shows % change, $ change, bar count and elapsed time; colour tracks direction (green up, red down). - On release the box stays. ✕ on the floating toolbar clears it; starting a fresh Shift+drag replaces the previous selection. - ↗ Sheet exports the bar range as a fresh spreadsheet — one row per bar with time, open, high, low, close, volume. ### Crosshair - Right-edge price pill — opaque coral tag at the cursor's y, mirroring the tooltip's price. - Crosshair sync across multiple charts is scoped by ticker — hovering an AAPL chart only moves other AAPL charts' crosshairs, never the BTC one next to it. ### Hotkeys - **1 / 2 / 3 / 4** — Switch timeframe to 1s / 1m / 1h / 1d. - **g** — Toggle grid lines. - **l** — Toggle linear / log price axis. - **a** — AUTO — refit price range to data. - **m** — Toggle magnet snap to OHLC. - **s** — Screenshot — composites bars, indicators, crosshair and watermark into a PNG download. > [info] Hotkeys fire only while the cursor is hovering the chart and ignore the active text input, so they don't collide with typing in the symbol search or other windows. ### Providers - **binance** — Spot. Live WebSocket on /ws/{symbol}@kline_{interval}. Fully wired, the default for most asset palette entries. - **bitmex** — Perp futures. WebSocket subscribe model. Picker entry exists; less-tested than Binance. - **okx** — Spot. REST polling. Picker entry exists; mapping for symbol formats is provider-specific. - **bitstamp** — Spot, USD pairs. REST. Same as OKX — usable, less polished. > [info] Don't expect a unified order book across providers — pricing differs by venue. Each chart is one provider; the palette spawns a fresh window per pick. ### What the top bar tells you - Search — type any symbol; ranking chips re-sort by Top Volume / Top Gainers / Top Losers. - Timeframe segmented control (1s / 1m / 1h / 1d). Disabled timeframes grey out per provider. - Live current price + 24 h change percent + RSI(14) — hides under 620 px wide. - ƒ Indicators dropdown + ⚯ Editor button (covered in the Indicators section). ### Watchlist & alerts Source: https://finterm.xyz/docs/watchlist > A line-per-symbol list with live prices and change percents, plus a simple alerts surface. The Watchlist window takes a free-text symbol list (one per line, e.g. BTCUSDT, ETHUSDT, SOLUSDT) and renders live last-trade + 24 h percent for each. Edit the list inline — changes save back to the workspace immediately. ### Alerts The Alerts window stores a list of price-threshold rules (symbol + above/below + price). The Finterm service worker fires browser notifications when crossed — grant notification permission once on first arm. Alerts are checked client-side via the same provider streams the chart uses. > [warn] Alerts only fire when the dashboard tab is open in some browser window. There's no server-side alerting (no email, no SMS, no webhook). For 24/7 coverage use your exchange's native alerts. ### News & reader Source: https://finterm.xyz/docs/news > Self-hosted RSS aggregator + click-to-open in-app article reader. The news feed pulls from CoinDesk, CoinTelegraph, Decrypt, The Block, Bitcoin Magazine, and Hacker News /best — server-side aggregated through /api/data/news/crypto, edge-cached for two minutes. Clicking a headline opens an in-app Reader window that fetches the article, strips chrome, and renders only the body. ### How extraction works The Reader's extractor (/api/extract) fetches the URL with realistic browser headers, parses the HTML through htmlparser2 (a SAX streaming parser, no DOM), and walks the tag tree picking up paragraphs, headings, lists, blockquotes, and images. If the direct fetch fails it falls through to a Wayback Machine snapshot. > [info] Extracted articles are SSRF-guarded: only public http(s) URLs are accepted; loopback, RFC1918, link-local, and cloud-metadata IPs are rejected at the gateway. Rate-limited at the heavy tier (60 req/min/IP). ## SEC & fundamentals ### SEC filings Source: https://finterm.xyz/docs/filings > EDGAR submissions list with form filtering and one-click open to the document on sec.gov. The Filings window pulls from /api/data/sec/submissions which proxies SEC EDGAR's submissions JSON. You search by ticker; we resolve to CIK via SEC's company-tickers manifest, then display the recent submissions with form / filing date / report date / primary document. ### Form filters Toggle the form chips (10-K, 10-Q, 8-K, S-1, 4, DEF 14A, 13F-HR, 20-F) to scope the list. The text-search box filters by form code or document description. ### Open the document Click any row to open the primary document at sec.gov in a new tab. URL is built from CIK + accession + document — no scraping, no proxy, the request goes direct to SEC. For an overview of what the filings viewer does, see the [free SEC EDGAR filings viewer](https://finterm.xyz/sec-filings), or read the guide on [how to read a 10-Q](https://finterm.xyz/blog/how-to-read-a-10-q). > [info] The palette has 'AMD filings' as a discoverable command per equity ticker — same window, pre-loaded. There are 30k of these (10k tickers × 3 actions: chart / filings / SEC) and the palette filter handles them with substring + ranking. ### Earnings & facts Source: https://finterm.xyz/docs/earnings-facts > us-gaap concept timeseries pivoted from EDGAR companyfacts. Twelve metrics × every reported quarter. The Earnings window calls /api/data/sec/quarterly which builds a per-period timeseries from EDGAR's companyfacts JSON. We pick the first matching us-gaap concept that has data for each metric — e.g. revenue tries Revenues, then RevenueFromContractWithCustomerExcludingAssessedTax, then SalesRevenueNet — so coverage is robust across industries. ### Synthesised Q4 10-Q filings carry Q1, Q2, Q3 data; only the 10-K carries the FY total. Where all three quarterlies and the FY exist, we synthesise Q4 = FY − (Q1 + Q2 + Q3) for flow metrics (revenue, net income, gross profit, operating income, R&D, opex). Stock-of-period balances (cash, equity, assets, liabilities) carry the FY closing value as Q4. To pull a clean quarter-by-quarter series yourself, see the [free SEC filings viewer](https://finterm.xyz/sec-filings) and the guide on [finding quarterly revenue in SEC filings](https://finterm.xyz/blog/find-quarterly-revenue-in-sec-filings). ### Available metrics - revenue, netIncome, grossProfit, operatingIncome - eps, epsDiluted - rd, opex - cash, equity, assets, liabilities > [warn] Some filers (notably Apple) report cumulative YTD values in the same XBRL contexts as quarterly facts. Per-period accuracy is generally good but spot-check the bars against the source 10-Q if you're using these for serious work. ## Indicator scripting ### How indicators work Source: https://finterm.xyz/docs/indicators-overview > The chart sandboxes your code in a Web Worker, runs it against the candle history, and plots the result. Click the ƒ Indicators button on any chart to open the presets menu. Picking a preset assigns its code to the chart's indicatorCode field and opens a TextEditor window bound to that field — you edit JS, the chart re-runs your function on every tick (throttled to ~5 Hz), and the returned series draws as polylines aligned bar-by-bar with the candles. ### The pipeline 1. You write a JS function body. The sandbox wraps it as `new Function('candles', 'OHLC', code)`. 2. Each tick (Binance WebSocket message), the chart pushes the candles array into a Web Worker. 3. The worker calls your function and posts the return value back. 4. The chart normalises the return value into { outputs, series } and hands the series to the renderer. 5. The renderer draws each series as a polyline; outputs render as labelled scalars in the chart's legend chip strip. ### Sandbox guarantees - Your code runs in a Web Worker — no access to the page DOM, window, document, fetch, or the user's data. - 1-second timeout per call. If your function loops forever the chart simply stops getting new outputs; nothing else breaks. - Errors are caught and surfaced as a toast in the editor — they don't crash the chart. - No persistent state across calls. Treat each invocation as pure: same inputs, same outputs. > [tip] The same sandbox runs the Backtester. If you've prototyped an indicator, the same `candles` array and the same return shape work in a backtest with a different `kind`. See Backtester docs. ### Scripting reference Source: https://finterm.xyz/docs/indicators-reference > Function signature, candle schema, return value contract, color palette. The full surface area. ### Function signature ```js function indicator(candles, OHLC) { // your code here return /* see return shapes below */; } ``` You don't write this declaration — the sandbox wraps your code body in a function that takes these arguments. Just use `candles` directly. ### candles Array of OHLC bars in oldest-first order. Each entry has the shape: ```ts type OHLC = { time: number; // unix seconds open: number; high: number; low: number; close: number; volume?: number; // present for binance / bitmex; may be missing for others }; ``` Length depends on what the chart has fetched plus what's streamed in since — typically a few hundred to a few thousand. The latest bar is `candles[candles.length - 1]` and updates in place as new ticks arrive. ### OHLC argument Currently undefined — the slot is reserved for a future helpers object. Don't depend on it. ### Return shapes Return any of these. The chart normalises whichever you pick into { outputs, series }. - **number** — Treated as a single named output, name='value'. No series drawn. - **{ name, value, color? }** — Single labelled scalar. Renders in the legend chip strip. - **Array<{ name, value, color? }>** — Multiple scalars. One chip each. - **{ outputs: [...], series: [...] }** — Combined: scalars in chips + per-bar series as polylines on the chart. - **null / undefined** — No output. Useful as 'not enough data yet' (e.g. inside the warmup window of a moving average). ### Series shape ```ts type Series = { name: string; // legend label values: (number | NaN)[]; // one entry per candle in the FULL history color?: string; // semantic alias OR raw CSS color }; ``` > [warn] values.length should equal candles.length. Use NaN (or null) for warmup positions where the indicator can't be computed yet — the renderer skips those points instead of drawing a line through zero. ### Color palette Series colors accept either a semantic alias (resolved against the active theme) or a raw CSS color string. - **'azure'** — var(--azure) — the cool accent (typically blue). - **'up'** — var(--up) — green / positive. - **'down'** — var(--down) — red / negative. - **'warn'** — var(--warn) — amber / caution. - **'ink'** — var(--ink-hi) — high-contrast text color, default. - **'#3b82c4' / 'rgb(...)'** — Any valid CSS color string passes straight through to the canvas stroke style. ### Examples Source: https://finterm.xyz/docs/indicators-examples > Working snippets for SMA, EMA, Bollinger Bands, Donchian, VWAP, RSI. ### Simple moving average ```js // SMA(20) drawn as a single polyline. const closes = candles.map(c => c.close); function sma(values, p) { const out = []; let sum = 0; for (let i = 0; i < values.length; i++) { sum += values[i]; if (i >= p) sum -= values[i - p]; out.push(i >= p - 1 ? sum / p : NaN); } return out; } const line = sma(closes, 20); return { outputs: [ { name: 'sma20', value: line[line.length - 1], color: 'azure' }, ], series: [ { name: 'SMA 20', values: line, color: '#3b82c4' }, ], }; ``` ### EMA ```js // EMA(21) — single line, NaN warmup. const closes = candles.map(c => c.close); const period = 21; const k = 2 / (period + 1); const out = []; let prev = NaN; for (let i = 0; i < closes.length; i++) { const v = closes[i]; prev = i === 0 ? v : v * k + prev * (1 - k); out.push(i >= period - 1 ? prev : NaN); } return { outputs: [{ name: 'ema21', value: out[out.length - 1], color: 'azure' }], series: [{ name: 'EMA 21', values: out, color: '#1e7a8c' }], }; ``` ### Bollinger Bands (20·2) ```js const closes = candles.map(c => c.close); const period = 20, mult = 2; const mid = [], up = [], lo = []; for (let i = 0; i < closes.length; i++) { if (i < period - 1) { mid.push(NaN); up.push(NaN); lo.push(NaN); continue; } let s = 0; for (let j = i - period + 1; j <= i; j++) s += closes[j]; const m = s / period; let v = 0; for (let j = i - period + 1; j <= i; j++) v += (closes[j] - m) ** 2; const sd = Math.sqrt(v / period); mid.push(m); up.push(m + mult * sd); lo.push(m - mult * sd); } return { outputs: [ { name: 'mid', value: mid[mid.length - 1], color: 'ink' }, { name: 'upper', value: up[up.length - 1], color: 'down' }, { name: 'lower', value: lo[lo.length - 1], color: 'up' }, ], series: [ { name: 'Mid', values: mid, color: '#8a4b2a' }, { name: 'Upper', values: up, color: '#c4554c' }, { name: 'Lower', values: lo, color: '#3f8a6e' }, ], }; ``` ### Donchian Channels (20) ```js const period = 20; const hi = [], lo = []; for (let i = 0; i < candles.length; i++) { if (i < period - 1) { hi.push(NaN); lo.push(NaN); continue; } let h = -Infinity, l = Infinity; for (let j = i - period + 1; j <= i; j++) { if (candles[j].high > h) h = candles[j].high; if (candles[j].low < l) l = candles[j].low; } hi.push(h); lo.push(l); } return { outputs: [ { name: 'hi', value: hi[hi.length - 1], color: 'up' }, { name: 'lo', value: lo[lo.length - 1], color: 'down' }, ], series: [ { name: 'High', values: hi, color: '#3f8a6e' }, { name: 'Low', values: lo, color: '#c4554c' }, ], }; ``` ### Cumulative VWAP ```js // Volume-weighted average of typical price, cumulative across history. const out = []; let pv = 0, vv = 0; for (const c of candles) { const tp = (c.high + c.low + c.close) / 3; const v = c.volume ?? 0; pv += tp * v; vv += v; out.push(vv > 0 ? pv / vv : NaN); } return { outputs: [{ name: 'vwap', value: out[out.length - 1], color: 'azure' }], series: [{ name: 'VWAP', values: out, color: '#5b6cc4' }], }; ``` ### RSI(14) as a scalar ```js // RSI is bounded 0–100, so it makes more sense as a chip readout // than as a polyline overlaid on price. Return a single scalar. const closes = candles.map(c => c.close); const period = 14; let up = 0, dn = 0; for (let i = 1; i <= period; i++) { const d = closes[i] - closes[i - 1]; if (d > 0) up += d; else dn -= d; } let rs = up / (dn || 1); let rsi = 100 - 100 / (1 + rs); for (let i = period + 1; i < closes.length; i++) { const d = closes[i] - closes[i - 1]; const u = d > 0 ? d : 0, dd = d < 0 ? -d : 0; up = (up * (period - 1) + u) / period; dn = (dn * (period - 1) + dd) / period; rs = up / (dn || 1); rsi = 100 - 100 / (1 + rs); } return { name: 'rsi14', value: rsi, color: rsi > 70 ? 'down' : rsi < 30 ? 'up' : 'azure' }; ``` ### Saving custom indicators Source: https://finterm.xyz/docs/indicators-custom > Persist your own scripts to the indicators library. Once you've tweaked an indicator into something you'd reuse, hit the + button at the bottom of the Indicators dropdown. Give it a name and an optional description; the script gets persisted to IndexedDB under chart.customIndicators.v1. Custom entries appear below the built-in presets in every chart, with a × to delete. ### Where they live - **IDB key** — chart.customIndicators.v1 - **Format** — Array of { id, name, description, code, createdAt } objects. - **Scope** — Per-browser. Not synced across devices, not shared between accounts. > [tip] Custom indicator IDs use a `custom:` prefix so they can't collide with the built-in presets. The chart stores the active indicator's ID on the window's indicatorName field, so reloading a workspace re-applies whichever indicator was running. ## Backtester ### Strategy backtester Source: https://finterm.xyz/docs/backtester-overview > Bar-by-bar JS strategy engine in the same Web Worker sandbox as indicators. The Backtester window loads OHLCV from any Binance symbol + interval, then runs your strategy function against each bar in sequence. Returns: trades + equity curve + P&L + max drawdown + win rate + buy-and-hold baseline. ### Strategy signature ```js // Called once per bar. Return a signal or null. function strategy(bars, ctx) { // bars = full bar history up to and including the current bar // ctx.i = current bar index // ctx.sma(n) / ctx.ema(n) — helpers, returns most recent value or null return /* 'BUY' | 'SELL' | null */; } ``` ### Default example — SMA crossover ```js const fast = ctx.sma(10); const slow = ctx.sma(30); if (fast === null || slow === null) return null; if (fast > slow * 1.001) return 'BUY'; if (fast < slow * 0.999) return 'SELL'; return null; ``` ### Engine semantics - Starting equity: 1000 units of quote currency. - BUY converts equity to position at the bar's close. SELL closes the position at the close. - No slippage, no fees modelled. Take the P&L as 'best case' and add your venue's fee schedule mentally. - BUY when already long is a no-op; SELL when flat is a no-op. > [info] Same Web Worker sandbox as indicators — same 1-second per-call timeout, same DOM-free environment. If your indicator code uses ctx.sma / ctx.ema you can drop it in here directly. ## Settings & theme ### Theme Source: https://finterm.xyz/docs/theme > Fifteen presets, accent hue picker, full token override via JSON. Finterm ships fifteen theme presets — sand (default Kepler paper-cream), high-contrast, ledger, mint, coral, sakura, nordic, solarized-light, night, mycelium, abyss, terminal, amber-crt, monochrome, and solarized-dark. Pick from the SettingsPanel or via the palette ('theme'). Each preset is a complete token set — paper / ink / accent / azure / up / down / warn / line / panel — so nothing in the UI clashes when you switch. ### Accent hue Independent of preset, you can spin the accent hue. The 'theme.accentHue' setting accepts 0–359; the default Kepler accent is 12 (burnt coral). The hue command in the palette shows a quick swatch picker. ### Per-token overrides Open the settings JSON ('settings' in the palette). Anything under theme.tokens overrides the preset's value. Order: preset defaults → accentHue → your overrides. Reset by deleting the override. > [info] Themes apply via CSS custom properties on the element. Components don't need restarting — changes take effect on the next animation frame. ### Settings panel Source: https://finterm.xyz/docs/settings-panel > Spec-driven GUI for every configurable knob, plus the JSON editor for advanced cases. Two ways to edit settings: the GUI (a categorised side-nav window) or the JSON editor (raw schema). Both write to the same store via lib/settings.ts; the JSON editor parses + validates and only commits on a clean parse. ### Categories - **theme** — Preset, accent hue, density, accent token overrides. - **chrome** — Top-bar layout (A / B / C), clock visibility, accent picker visibility. - **workspace** — Snap-to-grid, grid size, autosave on/off, autosave debounce. - **market** — Default chart provider, default timeframe, RSI period. - **privacy** — Anonymous telemetry on/off, crash reports on/off. ### Search Every setting is searchable from the command palette by its dotted path. Typing 'theme.accentHue' jumps you to that single field; typing 'snap' surfaces workspace.snapToGrid. ## Platform ### The data gateway Source: https://finterm.xyz/docs/data-gateway > /api/data/* — single backend route for every upstream provider, edge-cached at Cloudflare. Every market-data fetch the client makes goes through /api/data//, which is a single Cloudflare Worker route that dispatches to a typed fetcher in lib/server/providers/. The Worker emits Cache-Control headers so the Cloudflare CDN dedupes calls across all users — most reads never hit the upstream provider at all. ### TTLs by endpoint class - **price / ticker24** — 5 s — needs to feel live. - **funding / overview / news** — 30–120 s. - **exchange info / SEC submissions+facts** — 1 h — slowly-changing. - **sec/tickers, sec/companies** — 24 h — full registry, ~600 KB. ### Rate limiting Per-IP rate limits via Cloudflare's Workers Rate Limiting binding, four tiers (hot / default / heavy / bulk). Heavy and bulk additionally have a per-isolate burst cap to plug the eventual-consistency gap on short bursts. 429s come back as JSON with a Retry-After header. > [info] The gateway also fronts /api/extract (article reader) and /api/fetch-html (generic fetch). Both have an SSRF guard that rejects loopback / private / link-local / cloud-metadata IPs. ### Privacy & storage Source: https://finterm.xyz/docs/privacy > What we store, what we don't, what runs locally. ### What stays in your browser - All workspaces — windows, positions, sizes, chart symbols, indicator code, themes. - All files — TextEditor docs, spreadsheets, JSON. - All custom indicators. - All settings. ### What we send to a server - Market-data fetches go through our gateway; we don't log who asked for what. - Auth (if you sign up) — email + Argon2id password hash, NextAuth session cookie. Postgres on Neon, RLS-scoped role for app reads. - Optional anonymous telemetry — opt-in via privacy.telemetry. Off by default. ### What we do not send anywhere - Workspace contents. - Indicator code — runs entirely in your browser's Web Worker sandbox. - Files / spreadsheets — IndexedDB only. - Browser fingerprint, IP-tied tracking, marketing cookies. > [warn] Local-first means local-only for now. Clearing site data wipes everything. Sign in to enable cross-device sync — coming soon. ## Window Panels ### App Window Panels Reference Source: https://finterm.xyz/docs/windows > Every draggable window panel in Finterm — charts, SEC filings, watchlist, news, backtester and spreadsheet — with screenshots and usage notes. ## FAQ ### Common questions Source: https://finterm.xyz/docs/faq-general > Pricing, data sources, account, exchanges, mobile. #### Is it free? Yes — free during alpha. We'll surface paid tiers when there's a real cross-device sync to charge for. No payment information is collected today. #### Do I need an account? No. The dashboard works without signing in; everything persists locally. Sign up if you want it tied to an email so syncing (when it ships) connects across devices. #### Where does the price data come from? Crypto: Binance public REST + WebSocket. BitMEX, OKX, Bitstamp picker entries also exist. US equities: SEC EDGAR for fundamentals + filings; OHLCV via Alpha Vantage is wired backend-side but the chart UI doesn't yet consume it (use crypto charts for live OHLCV today). #### Can I trade through Finterm? No. There's no exchange-API-key field, no margin slider, no execute button. Watch + research only. #### Does it work on mobile? Yes — phones get an auto-tile mode where each window is full-bleed and you switch between them via a bottom tab strip. Charts, news, watchlists, and SEC tools work; Spreadsheet + Backtester are usable but were designed for desktop. #### Why no Bloomberg-like trade execution / OMS / RMS? Out of scope. Finterm is a personal terminal for individuals, not an institutional OEMS. If you need order management, broker-agnostic risk, or Bloomberg's IB chat — use Bloomberg. # Blog ## Free Bloomberg Terminal alternatives in 2026 Source: https://finterm.xyz/blog/bloomberg-terminal-alternative-free > Bloomberg costs $24,000 a year. Here's what you actually get for that, what's free, and where the free options fall short. Bloomberg Terminal is a data terminal + Excel add-in + news service + messaging network. The terminal itself is just the interface; what you're paying for is the data subscription. For most retail investors and analysts, most of that cost buys access to things that are either public domain or available through cheaper alternatives. ### What Bloomberg gives you - Real-time pricing across global equities, fixed income, FX, and derivatives - Analyst estimates and consensus data (not public) - Bloomberg Intelligence research - Company filings (publicly available on EDGAR for free) - Economic data (partly available via FRED for free) - The Excel BDH/BDP functions (unique to Bloomberg) - The Bloomberg messaging network (unique) ### What's free - SEC filings: entirely free on EDGAR and through Finterm's free EDGAR viewer (/sec-filings) - US equity prices: free on Yahoo Finance, various fintech APIs, Binance for crypto - Charts: free on TradingView's free tier and Finterm - Macro and economic data: free on FRED (api.stlouisfed.org), which is what Bloomberg re-serves - Fundamentals data: free via the SEC companyfacts API ### The alternatives - Finterm (free): SEC fundamentals, live crypto prices, charting, liquidations feed, backtester. No analyst estimates. - TradingView free tier: good charting, limited to 3 active indicators, 15-min delays on some markets - Yahoo Finance: real-time quotes, basic chart, screener, news. Ad-heavy. - Koyfin ($50-150/mo): cleaner fundamentals UI than Yahoo, analyst estimates on paid plans - EDGAR directly: all SEC filings, no frills ### What still costs money - Analyst estimates and consensus EPS/revenue are behind paywalls everywhere. Bloomberg, Refinitiv, Visible Alpha, and FactSet all charge for it. - The Bloomberg Excel add-in (BDH/BDP functions for pulling live data into spreadsheets) has no meaningful free equivalent. - Fixed-income pricing and credit data is genuinely not public. - If you need any of those three things, you're paying — the question is how much. > [info] For reading 10-Ks, tracking quarterly fundamentals, and charting US equities and crypto, the public data infrastructure is surprisingly complete. Bloomberg's defensible moat is analyst estimates, fixed income data, and the Excel functions. Everything else has a free path. ## Free stock charts online: what's actually free in 2026 Source: https://finterm.xyz/blog/free-stock-charts-online > Most 'free' charting tools delay the data, cap your indicators, or push you toward a paid tier. Here's what each option actually gives you. 'Free' in financial data usually means one of three things: delayed data, limited features gated behind a paid tier, or the product is ad-supported and your data is the revenue. The chart tools that call themselves free tend to be upfront about one of these and quiet about the other two. ### TradingView free tier Real-time data for US equities on major exchanges. Up to 3 active indicators per chart. One chart layout saved at a time. Screener access is limited. Drawing tools are mostly available. The free tier is genuinely useful for a single-chart workflow but the indicator cap becomes friction fast — you can't have volume, RSI, and a moving average on the same chart simultaneously. ### Yahoo Finance Real-time quotes with a basic candlestick chart. No drawings, no custom indicators, no multi-timeframe view. Works for a quick glance at price history. The chart is a commodity wrapper around the price feed, not a tool you'd use for serious technical work. ### Finterm [Finterm's chart](https://finterm.xyz/terminal) is a custom WebGL renderer — no TradingView widget underneath. Real-time Binance prices for crypto, same-session equity data. Unlimited indicators, including custom ones written in JS. No account required. The tradeoff is that it's browser-only with no mobile app, and asset coverage skews toward US equities and major crypto pairs. [Try it free.](https://finterm.xyz/terminal) ### What makes a chart actually useful - Data quality: delayed data is misleading for any time-sensitive analysis. Real-time or near-real-time matters. - Zoom and pan feel: a chart that stutters at 5,000 candles is a different product from one that doesn't. - Indicator flexibility: a 3-indicator cap means you can't have volume, RSI, and a moving average on one chart at the same time. - Drawing tools: trendlines, horizontal levels, Fibonacci retracements. These are table stakes. ### What you might still pay for - Multiple saved layouts with different assets and timeframes - Alerts (price or indicator-based) sent to email or phone - Screeners with more than a handful of filters - Historical tick data beyond daily or weekly resolution > [info] For charting a watchlist of stocks and crypto pairs with real indicators, TradingView free and Finterm together cover most of what most people actually use. The paid tier starts mattering when you need alert notifications, multiple saved layouts, or heavy screener use. ## Live crypto liquidations tracker: how to read forced liquidations Source: https://finterm.xyz/blog/crypto-liquidations-tracker > A forced liquidation is when an exchange closes a leveraged position automatically. Here's what the data shows and why traders watch it. When a leveraged position on a crypto exchange falls to the point where the collateral no longer covers the loss, the exchange closes the position automatically — a forced liquidation. The trader doesn't choose to sell; the exchange does it for them. These events show up in real time on Binance's public WebSocket feed, and the aggregate data tells you something about where leverage was concentrated and how quickly it's unwinding. ### Why forced liquidations matter Large liquidations can cascade. If a cluster of leveraged longs gets wiped out around the same price level, the forced selling pushes price down further, which triggers more liquidations below, which forces more selling. This is the mechanism behind sharp wicks on crypto charts — the candle body reflects willing sellers; the wick often reflects forced ones. The ratio of long vs. short liquidations also tells you which side of the market was caught wrong. A run of large long liquidations means price dropped faster than the leveraged bulls could cover; short liquidations mean price spiked faster than leveraged bears expected. ### Where the data comes from Binance publishes a forceOrder WebSocket stream that emits each forced liquidation in real time. Each event includes the symbol (e.g., BTCUSDT), the side (buy means a short was liquidated, sell means a long was liquidated), the quantity, and the price at execution. There is no equivalent public stream covering all exchanges, but Binance is large enough that its data is a reasonable proxy for cross-market leverage levels on major pairs. ### How to read the Finterm liquidations feed [Finterm's liquidations window](https://finterm.xyz/liquidations) streams the Binance forceOrder feed live. Each row shows the coin, side (long or short liquidated), size in USD, and the price at execution. Rows are colour-coded by side. Large individual liquidations above $500k tend to be meaningful; a steady stream of small ones at similar prices signals that a level is clearing. ### What to look for in practice - Size spikes: a $5M liquidation in a thin moment is different from the same dollar value spread across a hundred small orders. - Sustained one-sided flow: ten minutes of nothing but long liquidations means the market is pushing through a level where bulls had leverage parked. - Normalisation: after a large wick, watching liquidations drop back to background noise is one signal that the forced selling is done. > [warn] Liquidation data tells you where leverage was, not where price is going. It's a post-hoc signal — you're watching leverage clear, not predicting the next move. Use it as context alongside price and volume, not as a standalone entry trigger. ## Free EDGAR Search: How to Find Any SEC Filing Online Source: https://finterm.xyz/blog/free-edgar-search > EDGAR is the SEC's public database of every filing a US company has ever made. Here's how to search it for free, what each form type means, and a faster alternative to the official site. EDGAR — the SEC's Electronic Data Gathering, Analysis and Retrieval system — is a free public database of every filing a US-listed company has ever submitted to the SEC. 10-Ks, 10-Qs, 8-Ks, proxy statements, insider transactions: it's all there, and it's all free to access. You don't need a Bloomberg terminal or a paid data subscription to read a company's annual report. You just need to know where to look. ### How to search EDGAR The official site is at sec.gov/cgi-bin/browse-edgar. You can search by company name or ticker to pull up a filing history — every form the company has filed, ordered by date. Click any entry to see the filing index, which lists the actual documents: the full submission text, exhibits, financial statements. The search works, but the interface is unchanged from around 2002. There's no charting, no quarter-by-quarter data view, and no way to compare numbers across filings without doing the legwork yourself. ### What happened to FreeEDGAR? FreeEDGAR.com is an older third-party site that put a cleaner interface on top of the EDGAR database. It was widely used in the early 2000s when the SEC's own interface was essentially unusable, and many people still search for it by name out of habit. The SEC has improved its own site since then, but it's still not what most people would call user-friendly — no charting, no cross-filing data view, and a search UI that hasn't changed much in two decades. ### Form types: what you're looking at - 10-K — Annual report. The most detailed filing a company makes: audited financial statements, business overview, risk factors, and management's discussion of results. Filed within 60-90 days of fiscal year end. - 10-Q — Quarterly report for Q1, Q2 and Q3. Condensed financials, unaudited. No 10-Q for Q4 — that's folded into the annual 10-K. - 8-K — Material events. Earnings releases, executive changes, acquisitions, credit agreement updates. Companies have four business days to file. - DEF 14A — Proxy statement. Executive compensation, shareholder votes, board composition. - SC 13G / SC 13D — Large shareholder disclosures. Filed when an entity crosses 5% ownership. ### A faster way to read the numbers If you want to read a filing's full text, the SEC's EDGAR site works fine — it links straight to the documents. But if you want to see revenue, net income, earnings per share and cash flow across quarters without opening a dozen PDFs, there's a better path. The SEC publishes structured financial data through its companyfacts API at data.sec.gov. Every us-gaap concept a company has reported — revenue, EPS, operating cash flow, capex — is available as machine-readable JSON. Finterm is a [free EDGAR viewer](https://finterm.xyz/sec-filings) that reads this data and presents it as a quarter-by-quarter fundamentals table for any US-listed ticker. Type a ticker, see the filings list and the financial trend together, no account needed. > [info] The SEC's EDGAR API is public domain and has no rate limit for reasonable use. Every quarterly earnings figure for every US public company going back decades is sitting there for free. ### What you can't get for free EDGAR covers everything a company is required to file. What it doesn't have: analyst estimates, earnings call transcripts, alternative data, or real-time news. If you need those, a paid terminal is the right answer. For the actual source documents and reported financial history, free is sufficient. ## I built a financial terminal because Bloomberg costs $24k/year Source: https://finterm.xyz/blog/bloomberg-terminal-alternative-cloudflare-workers > Pulling SEC EDGAR data instead of paying Bloomberg $24k/year, caching with Cloudflare's edge instead of Redis, and building a WebGL chart renderer instead of bolting on TradingView widgets. The financial data industry runs on vibes and legacy software. Bloomberg Terminal: $24,000/year for a keyboard from 1983 and a UI that looks like it was designed by someone who genuinely hates users. Koyfin: prettier, but $600/year and you're still just renting access to the same SEC filings that are public domain. I built [Finterm](https://finterm.xyz/) — a keyboard-first browser terminal for stocks and crypto. Free tier is actually free, data comes from public sources, and the whole thing runs on Cloudflare Workers at the edge. Here's what building it taught me. ### The platform constraint that broke all my dependencies I deployed to Cloudflare Workers instead of a Node.js server. This was either the smartest or dumbest decision depending on the day. Workers runs V8 isolates, not Node. This means: no fs, no crypto module, no bcrypt, no axios, no cheerio, no jsdom. Half of npm just doesn't work. Password hashing is where I have to be honest: bcryptjs actually runs on Workers if you enable the nodejs_compat flag, which exposes most of node:crypto including crypto.randomBytes. Not a hard blocker. But I swapped it out for [@noble/hashes](https://github.com/paulmillr/noble-hashes) anyway — pure JS Argon2id, zero Node-compat surface area, and OWASP currently recommends Argon2id over bcrypt for new password storage because it's more memory-hard. The platform pushed me to look at the options, and the better option was right there. The SEC data layer was next. The old code used axios and cheerio to scrape HTML. Both fail on Workers. But scraping was wrong anyway — SEC's EDGAR has a proper JSON API at data.sec.gov that barely anyone uses. You get typed company facts, filing histories, the whole thing. No parsing required. The constraint forced the right architecture. ### Getting financial data for free (and why Bloomberg is a scam) Bloomberg's entire moat is: we have the data and you don't. But for US equities, the SEC requires public companies to file machine-readable XBRL data. Every quarterly earnings number, every balance sheet line item, every cash flow figure — it's all in the public domain at data.sec.gov/api/xbrl/companyfacts/. The fun part is that companies don't all use the same GAAP concept names. Apple reports revenue under RevenueFromContractWithCustomerExcludingAssessedTax. Other companies use Revenues. Some use SalesRevenueNet. You have to build a concept alias map and pick the first one that has data for the period you're looking for: ```typescript const CONCEPT_MAP = { revenue: [ 'Revenues', 'RevenueFromContractWithCustomerExcludingAssessedTax', 'RevenueFromContractWithCustomerIncludingAssessedTax', 'SalesRevenueNet', 'SalesRevenueGoodsNet', ], // ...30 more metrics }; ``` The other gotcha: SEC only gives you Q1, Q2, Q3, and FY. There's no Q4 row. You have to synthesise it: Q4 = FY minus (Q1 + Q2 + Q3). This works for flow metrics like revenue and net income. Balance sheet metrics are point-in-time — carry the FY close value as Q4 because it's the same date. Free cash flow isn't in XBRL at all. You derive it: FCF = operating cash flow minus capex. Capex is reported as a positive outflow, so you subtract it. > [info] Bloomberg charges you $24k/year partly for someone to have already figured all this out. The data is public. The EDGAR API is genuinely good. You just have to know where to look. ### A data gateway instead of Redis The obvious architecture for caching upstream API responses is: Redis. Call upstream, cache in Redis, return to client. This is also the architecture that costs $40/month for Upstash before you've written a line of product code. The alternative: route everything through a single Next.js API route and let Cloudflare cache it at the edge. ```bash GET /api/data/binance/ticker24?symbol=BTCUSDT GET /api/data/sec/quarterly?ticker=AAPL GET /api/data/fred/series?id=DFF ``` Every endpoint returns Cache-Control: s-maxage=N, stale-while-revalidate=86400. Price feeds get s-maxage=5. SEC filings get s-maxage=3600. Cloudflare's CDN absorbs cache hits before they reach the worker. Rate limiting only fires on cache misses — exactly the traffic you want to throttle. No Redis. No Upstash. No cache invalidation logic. Just HTTP semantics working as designed. ### Dual-layer rate limiting Cloudflare Workers has a native rate limiting binding — sub-millisecond, sliding window, in-memory at the edge. But it's eventually consistent across colos. During the ramp-up window, a burst of parallel requests from one IP can slip through before the state propagates. So there are two layers: - Layer 1 — Cloudflare's binding: 600 req/min for hot price feeds, down to 5 req/min for bulk registry fetches. Catches sustained abuse. - Layer 2 — Per-isolate in-memory burst cap: a Map that enforces a per-second limit from a single isolate's perspective. Doesn't share state across isolates, but nails the runaway curl loop and rogue browser tabs immediately while the CF binding catches up globally. ```typescript const BURST_PER_SEC = { heavy: 4, // 4/sec/isolate bulk: 1, // one registry fetch per second is plenty }; ``` The CF binding catches scraper farms. The in-memory burst catches the single person with a while(true) { fetch() } loop. Both are necessary. ### WebGL charts (not TradingView widgets) Koyfin's charts are TradingView widgets. They're fine. They're also somebody else's software bolted onto your product, and you can't touch the renderer. Finterm's chart is a custom WebGL renderer — candlesticks, volume bars, crosshair, axes, drawings, indicators, all built from scratch. The reason to go WebGL is simple: you want 5,000 candles to render in under a frame at 60fps. Canvas 2D can't do it. The DOM definitely can't. The tricky part was zoom/pan feel. The naive implementation: on wheel event, call zoomAt(factor, x). This produces stair-step motion — each wheel tick snaps the view to a new discrete state. TradingView doesn't do that, which is why TradingView feels smooth. The fix is RAF-eased momentum. Wheel events accumulate into a target zoom level; each animation frame eases toward it with a decay function. Pan has a sub-pixel offset — fractional bar positions accumulate in a float, and only integer amounts fold into the data slice. The residual is applied as a matrix translation so candles slide between positions rather than jumping bar-by-bar. > [warn] Getting this right took two weeks and about forty rewrites of the interaction handler. The zoom tuning constants are now codebase-documented with a note not to touch them. ### Indicators in a sandboxed Web Worker Users can write custom indicators in JavaScript. This is a sandbox problem. The solution is a Web Worker that evaluates user code with Function(), feeds it the candle history, and enforces a 1-second timeout. If the code hangs or throws, you terminate the worker and return an error — the main thread never blocks. The contract is minimal: candles in, { outputs, series } out. Users can implement VWAP, MACD, Supertrend in ~10 lines of JS. The built-in presets — SMA, EMA, Bollinger Bands, RSI — ship as code strings evaluated through the same sandbox, so there's exactly one code path. ### Pop-out windows and the ownerDocument problem Every chart window has a popout button that calls window.open() and renders the full React component tree into the opened window's document via createRoot. This is not a common pattern. The bug you hit immediately: addEventListener('mousemove', handler) resolves to the parent window's event target, not the popout's. Drag events stop working. matchMedia queries the parent viewport. innerWidth returns the wrong number. The fix is useOwnerWindow: ```typescript export function useOwnerWindow(ref: RefObject): Window { const [win, setWin] = useState(() => window); useEffect(() => { const next = ref.current?.ownerDocument?.defaultView; if (next && next !== win) setWin(next); }); return win; } ``` Every component that attaches DOM events uses this instead of the global window. Any document.createElement for offscreen canvas nodes goes through node.ownerDocument.createElement so it belongs to the right document. Plain JSX, fetch, and ResizeObserver are document-agnostic and just work. The only things that care about which window they're in are event listeners and layout queries. ### The state of financial tooling Bloomberg's defensible moat is mostly institutional inertia and the Excel add-in. Koyfin's moat is a better UX than Bloomberg, which is a low bar. Neither is particularly hard to build around for retail-scale use cases. The data is out there. The EDGAR API is excellent and underused. Binance's REST and WebSocket APIs are clean and fast. FRED has decades of macro data at api.stlouisfed.org. CoinGecko's free tier is genuinely generous. The hard part isn't access. It's building a data gateway that caches correctly, rate-limits sanely, and doesn't wake you up at 3am because you forgot to pay the Redis bill. [Finterm](https://finterm.xyz/) is live. Free to use. ## How to read a 10-Q Source: https://finterm.xyz/blog/how-to-read-a-10-q > A 10-Q is a company's quarterly report. Here's what's in it, how it differs from the annual 10-K, and how to read the numbers without a data terminal. A 10-Q is the report a US-listed company files with the SEC after each of its first three fiscal quarters. It's the quarterly companion to the annual 10-K, and it's where you go to see how a business is tracking between annual reports — revenue, earnings and cash flow for the most recent three months, while they're still fresh. ### How a 10-Q differs from a 10-K The 10-K is the annual report: comprehensive, audited, and filed within 60 to 90 days of the fiscal year end. The 10-Q is lighter — condensed financial statements that are reviewed but not fully audited, filed within about 40 days of quarter close. You get the numbers sooner, but with less detail and without the auditor's full sign-off. The other key difference: companies file a 10-Q for Q1, Q2 and Q3 only — never for Q4. The fourth quarter is folded into the annual 10-K instead, which is why a clean four-quarter series takes a little arithmetic. See [how to find a company's quarterly revenue in SEC filings](https://finterm.xyz/blog/find-quarterly-revenue-in-sec-filings) for why Q4 is always the missing piece and how to back it out. ### What's inside A 10-Q has two halves. Part I is the financials: condensed income statement, balance sheet and cash flow statement, plus the notes. Part II covers everything else — legal proceedings, updates to the risk factors first laid out in the 10-K, and any material changes since the annual report. For most readers Part I is the substance; Part II is where you check whether anything has materially changed. ### Reading it without opening the filing You don't have to open each 10-Q and read the statements by hand. Finterm is a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) that pulls the structured data behind every 10-Q and 10-K and lays it out quarter by quarter — revenue, net income, EPS and cash flow side by side — so the trend across filings is visible without a spreadsheet. If you're new to the filings themselves, start with [how to read a 10-K for free](https://finterm.xyz/blog/how-to-read-a-10-k-for-free). > [info] 10-Q numbers are unaudited. They're reviewed by the auditor but not given the full audit opinion a 10-K gets, so the occasional figure is restated when the annual report lands. For quarter-to-quarter trends that's rarely an issue, but it's worth knowing before you build a thesis on a single quarter. ### Filing deadlines Large accelerated filers (public float of $700M or more) must file a 10-Q within 40 days of the quarter's end. Accelerated filers ($75M–$700M float) also have 40 days. Smaller companies get 45 days. A company that can't meet this deadline files an NT 10-Q (notification of late filing) — visible on EDGAR and worth noting. Repeated late filings sometimes precede restatements. ### What to look for in Part II Part II is mostly boilerplate, but two items matter. Item 1A updates the risk factors from the most recent 10-K — any risk that's been added or materially modified since the annual report tells you something has changed in management's view of the business. Item 2 covers unregistered sales of securities, which surfaces small private placements and warrant exercises that may not otherwise make headlines. ### Year-to-date vs. quarterly: a common trap Some companies present income statement figures on a year-to-date basis in their 10-Qs rather than as discrete three-month figures. If Q3 revenue looks like it's three times Q1, check whether you're reading a nine-month YTD figure instead of the July–September quarter alone. The cover page of the 10-Q specifies the period; the income statement header will say either 'three months ended' or 'nine months ended'. Mixing the two when building a quarterly series gives nonsense results. ## How to find a company's CapEx in a 10-K Source: https://finterm.xyz/blog/find-capex-in-a-10-k > Capital expenditure is rarely labelled 'CapEx'. Here's where it sits in the cash flow statement, the names companies use, and how to read it per quarter. Capital expenditure — the cash a company spends on property, plant and equipment — is one of the most useful numbers in a filing and one of the more annoying to locate, because almost no company labels the line 'CapEx'. It lives in the cash flow statement under a name that varies by company and industry. ### Where it sits CapEx is in the investing-activities section of the cash flow statement, not the income statement. It's an outflow, so it's reported as a negative number. The most common line is 'Purchases of property, plant and equipment', but you'll also see 'Payments for capital expenditures', 'Additions to property and equipment', or capitalised-software costs broken out separately. ### The label problem In the SEC's structured data, most companies tag this as the us-gaap concept PaymentsToAcquirePropertyPlantAndEquipment. But capital-light businesses sometimes report only a broader investing line, and capital-heavy ones split maintenance from growth CapEx in the footnotes rather than on the face of the statement. If you're comparing companies, make sure you're pulling the same concept for each. ### Reading it per quarter Rather than open every 10-K and 10-Q and hunt for the line by hand, you can read CapEx straight from the SEC's structured data. Finterm is a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) that pulls PaymentsToAcquirePropertyPlantAndEquipment from companyfacts and lays it out quarter by quarter next to revenue and cash flow, so the trend is visible without touching a spreadsheet. CapEx is also the second half of free cash flow — see [how to calculate free cash flow from a 10-K](https://finterm.xyz/blog/calculate-free-cash-flow-from-10-k) for the full picture. > [warn] Don't read a single quarter's CapEx in isolation. Spending is lumpy — a new plant or data centre lands in one quarter and distorts the run rate. Look at CapEx as a percentage of revenue over several quarters to see the real intensity. ### Maintenance vs. growth CapEx Total CapEx from the cash flow statement doesn't distinguish between spending that keeps existing assets running (maintenance) and spending that adds new capacity (growth). Some companies break this down in the MD&A or in an earnings presentation; most don't. A rough proxy: compare CapEx to the period's depreciation and amortisation figure. When CapEx roughly equals D&A, the company is approximately maintaining its existing asset base. When CapEx significantly exceeds D&A, it's expanding. When CapEx is well below D&A, it may be harvesting old assets rather than reinvesting. ### Software companies and capitalised development costs Under ASC 350, software companies can capitalise internal development costs once technological feasibility is established — meaning those costs show up on the balance sheet as an intangible asset and are amortised over time, rather than flowing through R&D expense immediately. The cash for this spending shows up in the investing section of the cash flow statement, sometimes labelled 'capitalised software costs' or 'internal-use software development'. If you're computing FCF for a software company, decide whether this belongs in CapEx (economically it often does) and be consistent when comparing peers. ## How to find how much a company spends on R&D Source: https://finterm.xyz/blog/find-rd-spending-in-sec-filings > R&D spending sits on the income statement, but not every company breaks it out. Here's where to look, which us-gaap tag to use, and how to read it. Research and development spending tells you how much a company is reinvesting in future products. For software, pharma and hardware businesses it's often the single largest operating cost after the cost of revenue — but it isn't always shown as its own line, which makes it easy to miss. ### Where to look R&D is an operating expense on the income statement, usually labelled 'Research and development' just below cost of revenue and alongside selling, general and administrative expense. In the SEC's structured data it's tagged as the us-gaap concept ResearchAndDevelopmentExpense. ### When it's not broken out Companies in capital-light or non-technical industries frequently fold R&D into a broader operating-expenses line and never report ResearchAndDevelopmentExpense at all. If a company shows no R&D figure, that's usually the reason — not that the spending is genuinely zero. ### Read it as a share of revenue The absolute dollar figure matters less than R&D intensity — R&D divided by revenue. A 20% R&D-to-revenue ratio means very different things for a mature hardware maker and an early-stage biotech, so compare a company against its own history and its direct peers, not the market as a whole. Finterm reads ResearchAndDevelopmentExpense straight from companyfacts and puts it next to revenue per quarter, so the ratio is right there. It's a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) — type a ticker, no account. To pair it with the top line, see [how to find a company's quarterly revenue in SEC filings](https://finterm.xyz/blog/find-quarterly-revenue-in-sec-filings). > [info] Watch for capitalised development costs. Some companies capitalise part of their software or in-process R&D onto the balance sheet instead of expensing it, which understates the R&D line. The footnotes to the financial statements spell out the policy. ### Capitalised vs. expensed development costs GAAP requires most R&D to be expensed as incurred. But software development costs after technological feasibility is established, and certain in-process R&D acquired in a business combination, can be capitalised. Companies that capitalise aggressively show lower R&D expense on the income statement than their actual spending on development — the rest is on the balance sheet and flows through amortisation instead. The accounting policy note in the 10-K (usually within the first few notes to the financial statements) will describe the company's policy. ### What R&D intensity looks like across sectors R&D as a percentage of revenue varies enormously by industry. Pharma and biotech: 15–30% is common; early-stage biotechs often have no product revenue and spend almost entirely on clinical trials. Enterprise software: 15–25%. Consumer tech: 10–20%. Semiconductors: 15–25% given the long cycle times. Capital-intensive industries — energy, utilities, industrials, retail — typically report under 3%, often nothing. Comparing a semiconductor company's R&D intensity to a retailer's tells you nothing; comparing it to its peers tells you a lot. ## How to calculate free cash flow margin Source: https://finterm.xyz/blog/calculate-free-cash-flow-margin > Free cash flow margin is free cash flow divided by revenue. Here's the formula, what counts as a good margin, and how to track it quarter by quarter. Free cash flow margin answers a simple question: of every dollar a company brings in as revenue, how much ends up as cash it can actually do something with? It's one of the cleanest measures of business quality, because unlike profit margin it's hard to flatter with accounting choices. ### The formula Free cash flow margin = free cash flow ÷ revenue, expressed as a percentage. Free cash flow itself is operating cash flow minus capital expenditure (FCF = CFO − CapEx); see [how to calculate free cash flow from a 10-K](https://finterm.xyz/blog/calculate-free-cash-flow-from-10-k) for where those two inputs sit in the cash flow statement. ### What counts as a good margin There's no universal threshold, but a durable double-digit FCF margin is a strong sign — it means the business turns sales into cash without constant heavy reinvestment. Software and asset-light companies routinely clear 20-30%; capital-intensive businesses run far lower, and that's expected. Judge it against the company's own trend and its peers. ### Track it per quarter Both inputs — free cash flow and revenue — come straight from a company's SEC filings. Finterm is a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) that computes FCF = CFO − CapEx on the wire from companyfacts and shows it next to revenue per quarter, so the margin trend is easy to eyeball without exporting anything. > [warn] Free cash flow is lumpy quarter to quarter — working-capital swings and one-off CapEx can push a single quarter's margin well above or below the real run rate. Use a trailing-twelve-month figure, or average several quarters, before drawing conclusions. ### FCF margin vs. net income margin Net income is subject to accruals — revenue can be recognised before cash arrives, expenses can be deferred, and depreciation spreads a cash outflow across years. FCF margin is harder to manage through accounting choices: you either collected the cash or you didn't, and you either spent it on CapEx or you didn't. A company whose FCF margin consistently runs below its reported net income margin may be collecting revenue slowly (growing receivables), paying suppliers quickly (deteriorating working capital), or has CapEx running well above the depreciation line. All three are worth investigating. ### How FCF margin compresses Two things compress FCF margin: CapEx increases and working capital deterioration. Rising CapEx isn't automatically bad — a company investing ahead of planned revenue growth is behaving rationally, and the margin should recover as revenue scales into the new capacity. Working capital deterioration is a different signal: customers paying slower, inventory building up, or the company being squeezed by suppliers. If margins are compressing and CapEx is flat, the working capital section of the cash flow statement is usually where the explanation lives. ## What breaks when you ship Next.js on Cloudflare Workers Source: https://finterm.xyz/blog/nextjs-on-cloudflare-workers > A log of which npm packages don't survive the Workers runtime, what to swap them with, and the patterns that ended up mattering. > [info] Update (2026-05-16): The original version of this post claimed bcryptjs couldn't run on Workers because crypto.randomBytes wasn't polyfilled. That's wrong — the nodejs_compat flag exposes most of node:crypto including crypto.randomBytes. Corrected in the bcrypt section below. Thanks to the Cloudflare Workers team on Hacker News for the catch. I've been building Finterm, a financial terminal that runs entirely in a browser tab, on Cloudflare Workers via @opennextjs/cloudflare. The promise is the obvious one — global edge runtime, near-zero cold starts, pay-per-request pricing. The catch is that Workers don't run Node.js by default. Every package in your dependency tree either has to work in the Workers runtime natively or be reachable through the nodejs_compat compatibility flag, and a few common ones still don't fit either path. This is a rough log of what I had to rip out and what I replaced it with, plus a couple of patterns that turned out to be necessary on Workers but irrelevant on Node. ### bcrypt → Argon2id via @noble/hashes bcryptjs runs on Workers if you enable the nodejs_compat flag, which exposes crypto.randomBytes and most of node:crypto. I went with Argon2id instead for unrelated reasons: OWASP currently recommends Argon2id over bcrypt for new password storage, and @noble/hashes gives you a memory-hard implementation with zero Node-compat surface area at all, which felt cleaner for an edge runtime. The migration was a one-line change in the password hasher plus a re-hash on next login. ### cheerio / jsdom / parse5 → htmlparser2 For the news reader I needed to extract article bodies from arbitrary HTML. cheerio pulls in parse5, which doesn't run on Workers, and jsdom is even further away from compatibility. htmlparser2 is SAX-style streaming and pure JS, so it runs anywhere — but you have to walk the events yourself rather than querying a tree. Worth it for the runtime portability. ### The Yahoo Finance crumb handshake Not a Workers issue specifically, but I hit it harder because of Workers. Yahoo's quoteSummary, options, and profile endpoints require a session cookie and a crumb token bound to that session. The dance: first you GET https://fc.yahoo.com/, which returns 404 with Set-Cookie for A1 and A3; then you GET /v1/test/getcrumb with those cookies, which returns the crumb as plain text; then every gated request needs that cookie plus &crumb=... appended to the URL. ```typescript const seed = await fetch('https://fc.yahoo.com/', { headers: BROWSERY }); const cookie = seed.headers.getSetCookie() .map(c => c.split(';')[0]) .filter(c => c.startsWith('A1=') || c.startsWith('A3=') || c.startsWith('A1S=')) .join('; '); const crumbRes = await fetch('https://query1.finance.yahoo.com/v1/test/getcrumb', { headers: { ...BROWSERY, Cookie: cookie }, }); const crumb = (await crumbRes.text()).trim(); ``` A few gotchas. The getcrumb endpoint returns text/plain, not JSON, so you can't send an Accept: application/json header — it'll 406. The User-Agent has to look browsery; a plain curl/8.x or your fetch library's default gets rate-limited fast. The cookie and crumb stay valid for roughly thirty minutes, after which you re-handshake. I cache the pair per-isolate and refresh on the first 401. ### Single outbound gateway Workers isolates are short-lived but they do persist module state across requests on a single isolate, which makes the obvious caching patterns work better than on a per-request serverless model. Every upstream HTTP call in the app goes through one route — /api/data/[...path]. The route looks up the endpoint by path, runs the provider function, attaches Cache-Control: s-maxage=N, stale-while-revalidate=86400, and lets the Cloudflare edge cache do the heavy lifting. A four-tier per-IP rate limiter only fires on cache misses, so a popular ticker can be served to thousands of users without ever hitting Yahoo or SEC again that minute. This pattern also gave me a single chokepoint for an SSRF guard, so user-supplied paths can't reach back into Cloudflare's metadata service or some internal hostname. ### Pop-outs that share React state Off the Workers topic but a fun one. Each window in the dashboard has a popout button. It calls window.open('/popout.html'), then React-renders the same component switch into the popout's document. The popout writes back to the parent's workspace state through a callback, so edits round-trip in real time. Two non-obvious things had to change in the window-body components. Any addEventListener or matchMedia has to be bound to the popout's own window (I expose this through a useOwnerWindow(ref) hook). And any document.createElement for an offscreen canvas has to go through someExistingNode.ownerDocument.createElement so the node belongs to the popout's DOM rather than the parent's. ### Result Finterm is live at finterm.xyz — open it, type a ticker, and a chart, SEC fundamentals, options chain with Greeks, and analyst estimates open as draggable windows. Everything described above runs in Cloudflare's edge: no Node server, no cold starts to speak of. Happy to go deeper on any of the above if there's interest. ## How to read a 10-K for free (without a Bloomberg terminal) Source: https://finterm.xyz/blog/how-to-read-a-10-k-for-free > Where SEC annual reports actually live, the sections worth your time, and how to read them in a browser tab without paying for a data terminal. A 10-K is the most complete document a US public company publishes about itself — the annual report it's legally required to file with the SEC. It's also free. You do not need a Bloomberg or a paid data terminal to read one; you need to know where it lives and which parts are worth your attention. ### Where 10-Ks actually live Every 10-K, 10-Q and 8-K a company files is public on the SEC's EDGAR system the moment it's accepted. The catch is that EDGAR's own interface is built for compliance lookups, not for reading: you land on a list of accession numbers and raw document indexes rather than a clean view of the filing. That friction is the only thing standing between you and the data — and it's easy to remove. ### The sections worth reading first - Item 1 — Business: what the company actually does, in its own words. - Item 1A — Risk Factors: the risks management is willing to put in writing. Changes year-over-year are the signal. - Item 7 — MD&A: management's discussion of the results, the closest thing to a narrative of the year. - Item 8 — Financial Statements: the income statement, balance sheet and cash flow statement, plus the footnotes where the real detail hides. ### Reading them without the friction You can skip the accession-number maze entirely. Finterm is a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) that runs in a browser tab: type a ticker and the company's full filings list — 10-K, 10-Q, 8-K, DEF 14A — opens by date, each linking straight to the original document, alongside a quarter-by-quarter view of revenue, net income, EPS, equity and cash flow. No account, no install. > [tip] Read the latest 10-K next to the prior year's. The most useful signal in an annual report is usually what changed — new risk factors, shifts in segment revenue, language that got more or less confident. The filing is free, the data is the company's own, and the only thing a paid terminal really sells you on top is convenience. For reading the source, a browser tab is enough. ### The full document structure A 10-K is divided into four parts and fifteen numbered items. Most readers skip straight to Item 7 (MD&A) or Item 8 (financial statements), which is fine. But Items 9A (controls and procedures) and 9 (changes in and disagreements with accountants) are worth checking quickly — a change in auditor or a material weakness in internal controls is buried there and rarely makes headlines. ### The auditor's report The auditor's report is at the front of Item 8. In a clean filing it gives an unqualified opinion — the auditor has examined the statements and found them to present fairly in all material respects. Anything other than an unqualified opinion (a qualified opinion, an adverse opinion, or a disclaimer) is a serious signal. Going concern language — doubt about whether the company can continue operating for the next twelve months — can appear either in the notes or in the auditor's report and is the most consequential thing you can find in a 10-K. ### Comparing two years side by side The 10-K always includes prior-year comparative figures in the financial statements. The most useful analysis is often not the current year's numbers in isolation but what changed. New risk factors added since last year. MD&A language that shifted from confident to hedged. A segment that quietly shrank as a percentage of revenue. A liability that wasn't on last year's balance sheet. The prior 10-K is free on EDGAR and the comparison takes five minutes. ## How to find a company's quarterly revenue in SEC filings Source: https://finterm.xyz/blog/find-quarterly-revenue-in-sec-filings > Revenue is reported under inconsistent us-gaap tags across 10-Ks and 10-Qs. Here's how to pull a clean quarter-by-quarter series — and why Q4 is always missing. Quarterly revenue sounds like it should be the easiest number to pull from SEC filings. In practice, two things make it surprisingly annoying: companies tag revenue under different us-gaap concept names over time, and they never file a standalone report for the fourth quarter. Both are solvable. ### Why revenue is harder to pull than it should be The SEC's companyfacts feed exposes every numeric concept a company has reported, but 'revenue' isn't one fixed tag. A company might report under Revenues one year and RevenueFromContractWithCustomerExcludingAssessedTax the next after an accounting-standard change. Pull a single tag blindly and your series will have holes exactly where the company switched conventions. The fix is to pick the first available concept per metric from a ranked list of the names companies actually use, so the series stays continuous across reporting changes rather than breaking at the switch. ### The Q4 problem Companies file a 10-Q for Q1, Q2 and Q3, and a 10-K for the full year — but there is no 10-Q for Q4. So Q4 revenue is never reported directly. You back it out: Q4 = full year minus (Q1 + Q2 + Q3). For flow metrics like revenue and net income that arithmetic is exact; for balance-sheet items you carry the year-end figure forward instead. ### Doing it without a spreadsheet Finterm does both steps for you. It's a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) that reads companyfacts, normalises the concept names and synthesises Q4, then shows revenue, net income, EPS, equity and cash flow as a clean quarter-by-quarter table for any US-listed ticker — no account needed. > [info] If a quarterly series looks like it jumps or doubles, check whether the company reports year-to-date rather than discrete-quarter figures for that concept. Year-to-date values need to be differenced before they mean 'this quarter'. ### The us-gaap concept names that actually appear The most common revenue concepts in SEC structured data are: Revenues (the oldest, simplest tag), RevenueFromContractWithCustomerExcludingAssessedTax (introduced when ASC 606 took effect for fiscal years starting after December 2017), SalesRevenueNet, and SalesRevenueGoodsNet. A company that filed under SalesRevenueNet before 2018 and then switched to RevenueFromContractWithCustomerExcludingAssessedTax when ASC 606 hit will show a gap in either series. The fix is to check both and concatenate them at the switchover point. ### Segment revenue Many companies break total revenue into geographic or product segments in the notes to the financial statements. The structured XBRL data exposes this inconsistently — the top-line total is reliably tagged; segment-level figures are reported under company-specific extension tags that differ across filers and don't map cleanly to standard concepts. For segment revenue, you usually need to open the actual 10-Q or 10-K and read the segment note directly rather than relying on structured data. ## How to calculate free cash flow from a 10-K Source: https://finterm.xyz/blog/calculate-free-cash-flow-from-10-k > Free cash flow isn't a line item in SEC filings. Here's the formula, where to find the inputs in the cash flow statement, and the adjustments to watch for. Free cash flow is one of the most cited numbers in valuation, and it appears nowhere in a company's SEC filings as a line item. You compute it yourself from two figures that are both in the cash flow statement of any 10-K or 10-Q. ### The formula Free cash flow = cash flow from operations minus capital expenditure (FCF = CFO − CapEx). It's the cash a business generates after spending what it needs to maintain and grow its asset base — the cash actually available to pay down debt, buy back stock or sit on. ### Where to find the inputs - CFO — 'Net cash provided by operating activities', the subtotal at the bottom of the operating-activities section of the cash flow statement. - CapEx — usually 'Purchases of property, plant and equipment' (and sometimes capitalised software) in the investing-activities section. It's reported as a negative number, so you subtract its absolute value. ### Pulling it automatically Rather than dig the two figures out of every filing by hand, you can read them straight from the companyfacts data. Finterm is a [free SEC EDGAR viewer](https://finterm.xyz/sec-filings) that computes FCF = CFO − CapEx on the wire from a company's own reported us-gaap numbers, per quarter, with no proprietary 'adjusted' figures in between. > [warn] Watch for one-off items inside CFO — large working-capital swings or one-time tax events can make a single quarter's free cash flow misleading. Look at a few quarters of trend, not one print in isolation. ### Stock-based compensation: in or out? SBC is a non-cash charge that gets added back to net income to arrive at CFO in the indirect-method cash flow statement. That means CFO — and therefore FCF — is higher for companies that pay employees heavily in stock. Some analysts strip SBC back out before computing FCF, treating it as a real economic cost even if no cash changed hands that quarter. Others leave it in. Neither is wrong, but if you're comparing two companies and one pays 15% of revenue in SBC while the other pays 2%, the FCF numbers aren't comparable unless you apply the same treatment to both. ### What negative FCF means A negative FCF quarter doesn't automatically mean trouble. Early-stage businesses and capital-heavy expansions routinely generate negative FCF because investment is running ahead of revenue. The relevant questions are whether CFO is positive (the core business generates cash even if total FCF is negative) and whether the trajectory is improving as the investment matures. A mature business with persistently negative FCF when peers run at 20%+ margins is a different situation from a company that turned negative for one quarter due to a planned data centre buildout.