# Introduction > Flowscope is an order-flow trading terminal written in Rust, in the browser. Flowscope shows you what the order book and the tape are doing, as it happens. It draws footprint charts, liquidity heatmaps, order-book ladders and time & sales in a workspace laid out like TradingView, and renders everything on the GPU. Flowscope runs in the browser: the Rust engine and interface compiled to WebAssembly, drawn on the GPU. ## What Flowscope connects to Flowscope takes every market from the Flowscope data hub: one connection that carries every market, plus the history the hub recorded around the clock. The hub talks to each venue once for every client. Orders on live accounts are signed on your device and go straight to the venue. Without a hub the app has no market data; there is no mode on the venues directly. See [History and data](/docs/app/history-and-data#hub). | Venue | Markets | |---|---| | Binance | Spot | | Binance Futures | USDⓈ-M perpetuals | | Binance COIN-M | Coin-margined (inverse) perpetuals, `BTCUSD_PERP`; sizes shown in the coin | | Bybit | Linear perpetuals | | Bybit Inverse | Inverse perpetuals, `BTCUSD`; sizes shown in the coin | | OKX | Perpetual swaps | | Hyperliquid | Perpetuals | | Aster | Perpetuals | | Lighter | Perpetuals | | Coinbase | Spot (USD), with the whole order book | | Bybit Spot | Spot (USDT), 1 000-level books | | OKX Spot | Spot (USDT), 400-level books | | Deribit | BTC and ETH inverse perpetuals, USDC perpetuals; the whole order book | | Kraken | Spot (USD), 1 000-level books | | Deribit Spot | Spot: `BTC_USDT`, `ETH_USDT` and the other pairs Deribit matches itself; the whole order book | | All venues | One coin summed across venues, see [aggregated markets](/docs/app/aggregated-markets) | For every market it keeps the exact order book live (a snapshot plus every change, sequence-checked), every trade, open interest, funding and liquidations where the venue publishes them. The hub records all of it, the book once a minute, and every bar, footprint and heatmap is built from that recording. ## What you can see - A [liquidity heatmap](/docs/app/heatmap) of resting orders over time, with trade bubbles and the current book at the right edge. - [Footprints](/docs/app/footprint) with bid × ask, delta or volume cells, imbalances and naked points of control, over the whole loaded history. - A [DOM ladder, depth book and tape](/docs/app/dom-and-tape). - [Volume profiles and TPO](/docs/app/profiles) per visible range or per session. - [CVD and delta](/docs/app/cvd-and-delta), filtered by trade size, with one line per venue on aggregated markets. - [Bar Stats](/docs/app/bar-stats), a table of order-flow statistics under each bar. - [Liquidations](/docs/app/liquidations): markers, modelled liquidation levels and measured Hyperliquid take-profit, stop and liquidation maps. - [Gamma exposure](/docs/app/gamma-exposure) of BTC and ETH options: call wall, put wall and gamma flip from Deribit's option chains. - Classic [indicators](/docs/app/indicators) and [drawing tools](/docs/app/drawings), [alerts](/docs/app/alerts) and [bar replay](/docs/app/replay). - [Paper and live trading](/docs/app/trading) from the order ticket, the chart and the ladder. - Your own indicators and strategies in [Flowscope Script](/docs/scripting), with a Strategy Tester, and an [AI assistant](/docs/app/ai-assistant) that builds and tests them with you. Bars can be time-based from 100 ms to weeks, or built from a number of prints (`500T`), base volume (`25V`), notional (`$1M`), a price range (`10R`), Renko bricks (`20RK`) or the delta between buyers and sellers (`50DL`). ## Start here ```cards [Install](/docs/app/install) Run the browser version, or run on the simulator. [The workspace](/docs/app/workspace) Top bar, rails, panes, link groups and the bottom bar. [Charts](/docs/app/charts) Chart types, intervals, scales and chart settings. [Shortcuts](/docs/app/shortcuts) Every keyboard shortcut and mouse gesture. [Liquidity heatmap](/docs/app/heatmap) Read resting liquidity and how it changes. [Footprint](/docs/app/footprint) Bid × ask, delta and volume per price inside each bar. [History and data](/docs/app/history-and-data) What is kept, where it lives and how the data hub fills gaps. [Settings](/docs/app/settings) Environment variables, preferences and defaults. ``` ## Status Flowscope runs live in the browser, with paper trading and live trading on Hyperliquid, Binance, Bybit, OKX and Aster. > [!NOTE] > History reaches back as far as the data hub recorded a market: before that, a chart has no bars and no heatmap. No venue publishes historical order books, and Flowscope takes no candles or trade history from the venues. See [History and data](/docs/app/history-and-data). Source: https://flowscope.91-99-118-129.sslip.io/docs/app --- # Install > Build the browser version from source, or run it on the built-in simulator. Flowscope is built from source. The repository has one `Makefile` with every task; `make help` lists them. ## Requirements | Tool | Why | How to get it | |---|---|---| | rustup | Installs the pinned toolchain | [rustup.rs](https://rustup.rs) | | Rust 1.98.0 with `clippy`, `rustfmt` and the `wasm32-unknown-unknown` target | Pinned in `rust-toolchain.toml` | Installed automatically by rustup on the first `cargo` command in the repository | | `wasm-bindgen-cli` | Browser build only | `cargo install wasm-bindgen-cli --version --locked`, with the version from `Cargo.lock` | | `wasm-opt` (binaryen) | Browser build only, optional | Used for release builds when it is on your `PATH` | | Python 3 | `make serve` only | Serves the built files with `python3 -m http.server` | > [!IMPORTANT] > The `wasm-bindgen-cli` version must match the `wasm-bindgen` crate in `Cargo.lock` exactly. Look it up with `grep -A1 'name = "wasm-bindgen"' Cargo.lock`. ## Browser Flowscope runs in the browser. The browser version is the terminal compiled to WebAssembly. ```bash make web # release build into web/dist make web-dev # faster, unoptimized build into web/dist make serve # build, then serve web/dist on http://localhost:8080 ``` `make serve` takes a port: `make serve PORT=9000`. A plain static server has no `/hub`: add `?hub=` for a hub elsewhere; without a hub the app has no market data. `make dev` serves the build through the website, with a hub behind `/hub`. The build script compiles `flowscope-web` for `wasm32-unknown-unknown`, runs `wasm-bindgen --target web`, runs `wasm-opt -O3` on release builds when available, and copies `web/index.html` next to the output. The page draws into a ``. To host the build elsewhere, serve the contents of `web/dist` as static files. > [!NOTE] > The uncompressed bundle is about 13 MB, fonts included. `wasm-opt` and HTTP compression bring it down considerably. ### Differences from a native build | | Browser | |---|---| | Engine threads | None; the engine runs up to 8 ms per frame | | Memory budget for markets | 768 MB | | Layouts and preferences | `localStorage` | | Data hub | The website's `/hub`, or `?hub=` | | Fullscreen shortcut F11 | Use the browser's own | | Snapshots | Clipboard | ## Simulator Flowscope has a built-in synthetic exchange: random-walk prices, a deep book that churns and a steady tape, roughly like a busy BTC perpetual (150 trades and 10 book updates of 60 levels per second, 1 000 levels deep). Use it to try the interface offline or to measure performance. ```bash make serve # browser build; open http://localhost:8080/?sim ``` In the browser, any query string that contains `sim` starts the simulator. The bottom bar shows a **Simulator** status dot while it runs. ## Release builds `make web` packages Flowscope for others: a deployable static folder in `web/dist` with the optimised WebAssembly build, hashed and precompressed files, ready for any static host or CDN. ## Check a build ```bash make check # formatting, clippy (native and wasm), all tests make probe # connect to every venue headless and print what arrives ``` `make probe` is a quick way to confirm that your network reaches every venue before you open the app. ## Next steps ```cards [The workspace](/docs/app/workspace) Find your way around the window. [Settings](/docs/app/settings) Page parameters such as presets and the performance overlay. [Data](/docs/data) Where every market and its history come from. ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/app/install --- # The workspace > The parts of the Flowscope window and what each one does. The window follows TradingView's grammar, so you find things where you expect them: a top bar with the symbol and interval, a drawing-tool rail on the left, a watchlist on the right and panes in the middle. ```text ┌───────────────────────────────────────────────────────────────┐ │ Top bar: symbol · intervals · chart type · indicators · layout │ ├──┬─────────────────────────────────────────────────────┬──┬───┤ │ │ │ │ │ │T │ Panes │R │ P │ │o │ charts · DOM ladder · depth book · time & sales │a │ a │ │o │ │i │ n │ │l │ │l │ e │ │s │ │ │ l │ ├──┴─────────────────────────────────────────────────────┴──┴───┤ │ Bottom bar: ranges · go to date · status · time zone │ └───────────────────────────────────────────────────────────────┘ ``` ## Top bar From left to right: | Control | What it does | |---|---| | Symbol chip | Opens symbol search for the active pane. You can also just start typing a ticker. | | Favourite intervals | One click switches the active chart. Star intervals in the interval menu to add them here. | | Interval menu | Every built-in interval, your custom intervals and **Add custom interval…**. See [Charts](/docs/app/charts#intervals). | | Favourite chart types and chart type menu | Switch between candles, footprint, liquidity and the rest. With a footprint chart the menu also picks the cell type. | | **Indicators** | Opens the indicator dialog (/). See [Indicators](/docs/app/indicators). | | Indicator templates | Save the active chart's studies as a template, or apply one. | | Add pane | Adds a chart, footprint, liquidity heatmap, ladder, depth book, time & sales, liquidations or stats. Complete starting layouts are in Layouts → Community layouts. | | Undo, redo | Layout history (Ctrl+Z, Ctrl+Y). | | Layout menu | Save, autosave, copy, rename, create and switch layouts. See [Layouts](/docs/app/layouts). | | Performance | Toggles the [performance overlay](/docs/app/performance). | | Chart settings | Colours, status line, scales and canvas of the active chart. | | Snapshot | Copies the window image to the clipboard. | | **AI** | Opens the [AI assistant](/docs/app/ai-assistant) (Alt+A). | | **Trade** | Opens the order ticket and the account panel (Alt+O). See [Trading](/docs/app/trading). | The layout name shows a dot (`•`) when it has unsaved changes and autosave is off. ## Panes Every pane has a header strip with: - a **link chip**: put panes in link group 1 to 5 and they share the symbol. Changing the market on one changes it on all panes in the group. Charts of a group also share their **drawings** (a line drawn, moved or removed on one chart is on the others too; Settings › General › Charts › *Linked charts share drawings*), and, with *Linked charts share the interval*, their interval. With *the crosshair stays in the link group* on, the crosshair of a linked chart shows only on the charts of its group. - the **symbol** and, on charts, an **interval chip** that opens the interval menu for that pane. - **maximize / restore** and **close** buttons. Double-click the free space of a header to maximize the pane. Maximizing draws that one pane alone and leaves the rest of the layout untouched, so restoring loses nothing. Drag the borders between panes to resize them. The pane you last clicked is the **active pane**; the top bar, symbol search and dialogs act on it. Pane types: | Pane | Page | |---|---| | Chart (any chart type) | [Charts](/docs/app/charts) | | Order book ladder | [DOM and tape](/docs/app/dom-and-tape) | | Order book depth | [DOM and tape](/docs/app/dom-and-tape#depth-book) | | Time & sales | [DOM and tape](/docs/app/dom-and-tape#time-and-sales) | | Stats | [Stats](#stats) below | ### Stats {#stats} The **Stats** pane (**Add pane → Stats**) shows a market at a glance: the price and its 24 h change; for perpetuals and aggregated markets the funding rate, the time to the next funding settlement and open interest in USD; then, over a window of 1 m, 5 m (default), 15 m, 1 h or 4 h (chips at the top), the market buys and sells with their count and value, a bar of the buys' share of the value with the difference, and the long and short liquidations. On an aggregated market every figure covers all its venues. Like every pane it follows its link group's market. ## Left rail: drawing tools The tool rail holds the drawing tools in groups, then **Measure** and **Zoom in**, then the magnet, stay-in-drawing-mode, lock all, hide all and remove all drawings on this chart. See [Drawings](/docs/app/drawings). ## Right rail and panel Two icons on the far right open the side panel. Click the open panel's icon again to close it. ### Watchlist and details The watchlist lists markets with last price and 24-hour change. Click a row to show it on the active pane. **+** adds a symbol. Right-click a row to remove it, add an alert, or **open it in a new pane** of any kind: chart, footprint, liquidity heatmap, ladder, depth book, time & sales or [Stats](#stats). **Columns** chooses the optional columns: 24 h change, 24 h volume (USD), funding rate, open interest (USD) and **trades per minute** (prints over the last minute, a live measure of activity). Click a column head to sort by it. Columns that do not fit leave from the right. The **treemap** button shows the watchlist as a treemap instead: each market a tile with its area by 24 h volume and its colour by the 24 h change (brighter the further it moved), the active market outlined. Click a tile to show it; right-click for the same menu as a row. Below it, the detail card shows the active market: pair, venue and type (spot, perpetual, or aggregated spot + perpetuals), last price, 24-hour change and key statistics: | Statistic | Meaning | |---|---| | 24h high, 24h low, 24h volume | From one-minute candles | | Spread | Best ask minus best bid | | Bid depth −1 %, ask depth +1 % | Resting size within 1 % of the price | | Open interest, funding | Derivatives only | | Liquidations 1h | Derivatives only | | Book levels, prints held, memory | What the engine holds for this market | ### Object tree and data window - **Object tree** lists the active chart's indicators and drawings. Indicators have show or hide, settings and remove buttons; drawings have show or hide, lock and remove. - **Data window** lists the values under the crosshair: OHLC, volume and every study value. ## Bottom bar | Element | What it does | |---|---| | Range presets | `1D` (1m bars), `5D` (5m), `1M` (30m), `3M` (1h), `6M` (2h), `1Y` (1D), `All` (1D, everything loaded). Each switches the active chart's interval and fits the span. | | Go to date | Jumps the active chart to a date. | | Simulator dot | Shown when running on the simulator. | | Connection status | Venue and connection state of the active market. | | Engine summary | Markets open, market memory, events per second and frame rate. | | Version | The running Flowscope version. | | Clock and time zone | Click to pick UTC, local time or a fixed offset. Charts and the clock use the chosen zone. | ## Phones and narrow windows Below about 720 points wide (a phone, or a narrow browser window) the terminal switches to a phone layout; wider windows keep the desktop layout. Nothing about your layout changes: widen the window and every pane is where it was. | Part | On a phone | |---|---| | Top bar | Menu, symbol, interval, chart type and indicators; the ticket, your account and **⋮** (undo, redo, snapshot, chart settings, bar replay, templates, add pane, layouts) | | Panes | One at a time; the strip under the top bar lists the layout's panes, tap one to show it | | Tab bar | **Chart**, **Watchlist**, **Trade** (the order ticket), **Alerts** and **More** (object tree, script editor, strategy tester, account panel, go to date, settings) | | Script editor | Full screen; **Chart** returns to the chart | | Dialogs | Nearly full screen; settings wrap their labels, the indicator list puts its sections in a menu | On the chart, drag to pan (a flick keeps gliding), pinch to zoom, drag the price or time axis to stretch it. In the browser the page itself does not zoom, so a pinch always goes to the chart. Added to the home screen, Flowscope opens without the browser bar. Between about 720 and 1100 points (tablets, small windows) the desktop layout stays; the bottom bar folds its range presets into a **Range** menu and shortens its labels. ## Crosshair The crosshair time is shared across charts: hovering one chart shows the same moment on the others. ## Snapshots The camera button copies an image of the window to the clipboard. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/workspace --- # Charts > Chart types, intervals, scales and the chart settings dialog. A chart is a main price pane with optional study panes below it, all sharing one time axis. It draws in a fixed order, back to front: grid, heatmap, session profiles, volume, the main series, overlay studies, order-flow layers, drawings, axes, study panes, crosshair and the legend. ## Chart types Pick the chart type from the top bar. Star a type in the menu to keep it as a one-click button; Candles, Footprint and Liquidity are starred by default. | Group | Types | |---|---| | Bars | Bars, Candles, Hollow candles | | Lines | Line, Line with markers, Step line | | Areas | Area, Baseline | | Columns | Columns, High-low | | Order flow | Footprint, TPO (Market Profile), Liquidity (price line) | | Transformed | Heikin Ashi | The order-flow types have their own pages: - **Footprint** draws bid × ask cells per price row inside each bar. See [Footprint](/docs/app/footprint). - **TPO (Market Profile)** draws letters per period at price for each session. See [Profiles](/docs/app/profiles#tpo). - **Liquidity (price line)** draws no bars: just the heatmap and a price line, for reading liquidity. See [Liquidity heatmap](/docs/app/heatmap). Each type has its own default bar width: 84 px for footprints, 6 px for TPO, 4 px for liquidity and 8 px for everything else. ## Intervals Flowscope builds bars from seven kinds of specification. You can type them in the custom interval dialog in the same spelling the menu uses. | Kind | Spelling | Examples | Built from | |---|---|---|---| | Time | `ms`, `s`, `m`, `h`, `D`, `W` | `100ms`, `5s`, `15m`, `4h`, `1D`, `1W` | Whole minutes: the minute footprint store and the data hub's candles, built from the same recording. Sub-minute: the raw tape. | | Ticks (prints) | `T` | `100T`, `500T` | The raw tape | | Volume (base units) | `V` | `25V` | The raw tape | | Notional (quote) | `$` prefix with `K`, `M`, `B` | `$100K`, `$1M` | The raw tape | | Range (ticks) | `R` | `10R` | The raw tape: a bar spans at most this many price ticks | | Renko (ticks) | `RK` | `20RK` | The raw tape: bricks on a fixed grid, wicks from the prints | | Delta (base units) | `DL` or `Δ` | `50DL` | The raw tape: a bar closes once buyers and sellers differ by this much, either way | The shortest time bar is 100 ms. The interval menu offers these sections. Seconds, Minutes, Hours and Days are expanded by default. | Section | Intervals | |---|---| | Ticks | 10T, 50T, 100T, 250T, 500T, 1000T | | Seconds | 1s, 5s, 10s, 15s, 30s, 45s | | Minutes | 1m, 2m, 3m, 5m, 10m, 15m, 30m, 45m | | Hours | 1h, 2h, 3h, 4h, 6h, 8h, 12h | | Days | 1D, 3D, 1W | | Notional | $100K, $500K, $1M, $5M | | Range | 4R, 10R, 20R, 50R, 100R | | Renko | 10RK, 25RK, 50RK, 100RK | | Delta | 10DL, 50DL, 100DL, 500DL | **Add custom interval…** opens a dialog with a number and a unit: ticks, seconds, minutes, hours, days, volume, notional (USD), range, Renko or delta. Custom intervals appear in their own section at the top of the menu. Flowscope stores intervals in a canonical spelling, so `60m` and `1h` are the same interval. Favourite intervals sit in the top bar. The defaults are `1m`, `5m`, `15m`, `1h`, `4h` and `1D`. > [!TIP] > The heatmap, trade bubbles and the open-interest line follow the bar's real start and end times. They line up with tick, volume and notional bars exactly as they do with time bars. ## Navigating Charts handle like TradingView. The short version: - Drag the plot to pan. Dragging vertically leaves auto scale. - The wheel zooms time with the right edge fixed. - Drag the price axis to stretch prices and the time axis to stretch time. Double-click an axis to reset it: the price axis fits the visible candles again. Right-click the price axis for auto, logarithmic and percent scale, and with the order-book heatmap **Fit heatmap depth**. - A flick keeps gliding. The `»` button returns to the live edge. - Alt+R resets the view. Every gesture is listed in [Shortcuts](/docs/app/shortcuts). Scroll and zoom are kept per chart, so undo, redo and reloading a layout keep your view. ## Scales Right-click the plot for the context menu: | Item | What it does | |---|---| | Reset chart view | Same as Alt+R | | Auto scale | Fit the price scale to the visible bars | | Logarithmic scale | Log price axis | | Add indicator… | Opens the indicator dialog (/) | | Chart settings… | Opens the chart settings dialog | | Remove drawings | Shown when the chart has drawings for this market | Study panes have their own value axis. Drag it or use the wheel over it to stretch, and double-click it to fit the data again. Overlapping value tags on an axis are moved apart so each stays readable. The live price tag never moves. ## Chart settings Open **Chart settings** from the gear in the top bar or the context menu. Edits apply live. | Tab | Settings | Defaults | |---|---|---| | Symbol | Body, wick and border, each shown or not with its up and down colours (the border takes the body's colours until set); the line colour and width of line and area charts; the area's fill colour (unset: the line colour fading out) | Bodies and wicks up `#22AB94`, down `#F23645`, no border; line `#2962FF`, 2 px | | Price marker | The last price as Market Monkey marks it: Axis label (a tag on the scale), Inline label (in the plot), Price line, Time countdown and the line's width (1–5); all off leaves the last price unmarked | Axis label, price line and countdown on, 1 px | | Status line | The price pane's status line, its symbol and prices row, OHLC values (with volume and delta), indicator titles, indicator values | All on | | Scales | Logarithmic, indicator values on the scale, executions; Scale padding top and bottom: room above and below the prices when the scale fits them, in percent (Auto follows Settings › Chart interaction) | Linear, Auto | | Canvas | Grid lines, symbol watermark | Grid off, watermark off | Prices print as order-flow terminals print them, without thousands separators (`81559.9`) on the scale, in legends and in the DOM; trading amounts keep their grouping. The status line names the venue by its id (`BTCUSDT · 30m · BINANCEF`), reads `O H L C` and the change as `+12.5 +0.02%`. Order-flow panes (Delta, CVD, Open Interest, Liquidations) write their values with two decimals (`9.13`, `240.77K`); a CVD's legend shows its running delta alone. ## Chart background Put a picture behind a chart with **Background…** in the chart's menu. Pick one of your own uploads or a FlowScope background from the catalog. Edits apply live; **OK** keeps them and **Cancel** puts the chart back as it was. **Your own pictures.** Click **Upload picture…** or drop a picture file on the dialog (you need to be signed in). Any common picture works: JPG, PNG, WebP, GIF, BMP, TIFF and ICO everywhere, and HEIC or AVIF in browsers that can open them (Safari opens HEIC). You don't need to resize or convert anything. Large photos are reduced to at most 2560 pixels on their long side and compressed until they are under about 480 KB. Photos taken sideways are turned upright, and pictures with transparent areas keep them. Each upload shows its final size, for example `2560×1440 · 312 KB`. You can keep five uploads, 2 MB in all. For the best results use a landscape picture (16:9) of at least 1920×1080. **Look.** A preview with sample candles shows the result before you leave the dialog. | Setting | What it does | |---|---| | Readability | One click for a tested look. **Subtle** is faint and soft, **Balanced** shows the picture with the candles in front, **Vivid** is strong and sharp | | Opacity, Tint | How strongly the picture shows, and a colour it is tinted with | | Fit | **Fill** covers the chart and crops what doesn't fit, **Fit** shows the whole picture (the rest stays chart background), **Stretch** stretches it to the chart, **Tile** repeats it at its own size | | Keep in view / Position | Which part of the picture stays visible when Fill crops it, or where Fit and Tile place it | | Blur | Softens the picture (up to 20 px) so candles and lines stand out | | Black and white | Shows the picture in greys, so the only colours on the chart are the market's | **Defaults** removes the background. **Reset look** keeps the picture and resets everything else. ## The legend The status line at the top left of each pane shows the symbol and the OHLC of the hovered bar on the main pane, and one row per study with its values. Hover a study row for the eye (show or hide), settings and remove buttons. Double-click a row to open its settings. ## Study panes Studies placed in a pane appear below the price pane. Drag the separators between panes to change their heights. The main pane starts at three times the height of each study pane. ## History while you scroll Every frame a chart tells its market how far back it is looking. The market then loads candles back to that point and, for footprint charts, the footprints of the visible range. Arriving history patches the bars in place. See [History and data](/docs/app/history-and-data). Source: https://flowscope.91-99-118-129.sslip.io/docs/app/charts --- # Layouts > Included community layouts, a clean chart to start, autosave, undo and redo, and indicator templates. A layout is the arrangement of panes with everything in them: each chart's market, interval, chart type, studies, drawings and colours, the ladder and book settings, link groups and the watchlist. Flowscope stores it as JSON. ## Starting a layout On first launch, Flowscope opens one **BTCUSDT perpetual candlestick chart at 15m**, with volume below and no background grid. **Create new layout…** starts with the same simple chart, using the market and interval of your current chart. It does not copy its studies or drawings. Your existing layouts and watchlist stay available. **Add pane** adds one chart or widget to the open layout. For a complete workspace, choose **Layouts → Community layouts**. Four layouts by Flowscope are included, with no sign-in or catalog download required: | Layout | What it opens | |---|---| | Liquidity & execution | A large 1m Binance BTCUSDT perpetual heatmap with trade bubbles and book profile, alongside a linked DOM and tape | | Footprint desk | A large 5m bid × ask footprint with CVD, bar statistics, VWAP and visible-range volume profile, plus a 30m context chart | | Market profile | Daily TPO sessions with 30-minute letters, value area, POC and initial balance, plus a 15m chart with VWAP and volume profile | | BTC & ETH momentum | Binance BTCUSDT and ETHUSDT perpetual charts at 15m, each with volume, CVD and open interest | Each opens as a new, private, editable copy. Context charts and the BTC/ETH comparison are unlinked, so changing a market or interval does not overwrite their different roles. The liquidity chart, DOM and tape share one market link. These are analysis workspaces, with no automated trading strategy attached. Developer and load-test presets (the 60-chart and 100-chart stress layouts) open with `?preset=` in the page URL; see [Settings](/docs/app/settings#page-parameters). ## Named layouts The layout menu on the right of the top bar manages your layouts. | Item | What it does | |---|---| | Save layout | Writes the current panes to the open layout (Ctrl+S) | | Autosave | When on, every change is written to the open layout as you work | | Make a copy… | Saves the current panes under a new name | | Rename… | Renames the open layout | | Delete layout… | Deletes the open layout; click a second time to confirm | | Create new layout… | Starts a clean candlestick chart with volume on the current market and interval | | Layouts | Every saved layout with a short summary (its first chart and how many charts it has). Click to open; star to keep it at the top; right-click to delete | You cannot delete the last remaining layout. With autosave off, the layout name in the top bar shows `•` while there are unsaved changes. > [!TIP] > Autosave is on by default. Turn it off if you want to experiment and keep a known-good layout, then save explicitly with Ctrl+S. ## Community layouts **Community layouts** at the top of the layouts menu opens the four included Flowscope layouts and layouts other traders share. Search by name or description, order by **Popular** (most opened) or **Newest**, and filter by what a layout shows: footprint, heatmap, TPO, DOM, tape, several charts, all venues, scripts. Each card draws the layout's panes; pick one to see what is inside, then **Open as new layout**. It arrives as a copy among your layouts and becomes the open one; your layouts and your watchlist stay as they are. Browsing needs no account. The included layouts remain available if the community service is unavailable. Popular/Newest sorts community publications; the included collection keeps its curated order. ### Sharing your own Every layout is **private**: only you see it, on your devices. Signed in, **Share this layout…** in the layouts menu opens its **Manage** form (the same as the pencil next to it in the Library): - **Name, description, category**: how it is listed and found. - **Visibility**: **Private** (you alone), **Invite only** (the people you invite by email or @username, each until a date you choose; an address without an account waits until it signs up) or **Public** (listed in the community). - **Share via link**: a secret link that opens it without listing it anywhere. Copy it from the form; turning it off ends the link. - **Available until**: after this date it closes for everyone but you. - **Hide author name**: listed as "Anonymous". **Save changes** sends what you changed; **Stop sharing** takes the copy down with its link, invitations and stars. Layouts others already opened stay theirs. A shared layout never includes your watchlist, accounts, alerts or keys; scripts on its charts go along as their code. ## Library **Layouts › Library…** (also **Indicators › Library & sharing** and the command palette) manages indicators, layouts and templates in one window. Pick the kind on top, then where they come from on the left: | List | What it holds | |---|---| | Mine | Your own, each with how it is shared; the open layout is marked **Active**. The pencil opens **Manage** | | Starred | What you starred, and your own starred layouts and templates | | Shared with me | What others invited you to, with when your invitation ends; open a copy or remove it from the list | | Community | Public items with their author ("Anonymous" when hidden) and stars | | Built-in | The indicators and layouts that come with Flowscope | Search looks through names, descriptions and categories; order by **Stars**, **Newest** or **Name**. Opening someone else's item adds a private copy to yours. Paste a secret link under **Open a link** to open what it points to. Everything is free; stars and sharing need an account. ## Undo and redo Every change to the layout is an undo step: adding or closing a pane, changing an interval, moving a drawing, editing a study. Flowscope compares the layout every 400 ms while the pointer is idle and keeps the last 60 steps. | Action | Keys | |---|---| | Undo | Ctrl+Z | | Redo | Ctrl+Y or Ctrl+Shift+Z | On macOS use Cmd instead of Ctrl. Undo, redo and opening a layout rebuild the panes, but every chart keeps its scroll position and zoom. ## Indicator templates A template is a saved set of studies and scripts with all their inputs, styles, timeframes and data sources. It also includes the chart's footprint settings and aggregation, or its TPO mode, and any Global row value used by those layers. A footprint by itself counts as an indicator. Apply the set to any chart in one click. - **Save:** open the templates button next to **Indicators** and choose **Save indicator template…**, or **Save chart as template…** under **Templates** in the indicator dialog. The suggested name is always a new one; saving under an existing name replaces that template and keeps its star. A chart without indicators cannot be saved. - **Apply:** click a template in the same menu, or under **Templates** in the indicator dialog. Its indicators replace the chart's own; Shift+click (or **+** in the dialog) adds them beside. A message names the chart it went to, and Ctrl+Z undoes it. - **Which chart:** templates go to the active chart. While an order book, DOM or tape is active, they go to the chart that was active last. - **On chart:** the template the chart shows now has a check mark in the menu and an **On chart** badge in the dialog. - **Star** a template to list it first. **Update** takes the chart's current indicators into it; **Delete** asks for a second click. The reusable template library lives in preferences and is available in every layout. Each layout stores its actual settings and the selected template identities independently; it does not depend on the template still existing in the library. Legacy templates retain the fields they originally saved; saving or updating them captures the newly supported data settings. ## What is stored where Layouts and preferences are kept separately. Both are repaired on load, so a value written by an older version never stops the app; an unreadable entry is replaced by its default and a warning is logged. | Item | Storage key | |---|---| | Layouts (workspace) | `flowscope-workspace-v3` | | Preferences: favourites, custom intervals, templates, time zone, autosave, drawing options, recent symbols | `flowscope-prefs-v1` | | Which side panel is open | `flowscope-panel-v1` | They live in `localStorage` for the page's origin, and in your account when you are signed in. ## Several windows or tabs You can open Flowscope in several browser tabs at once. - **Layouts, settings, scripts, alerts and AI chats are shared.** What you change in one window reaches the others within a second. If two windows changed different things, both changes are kept: a layout you made in one window and a setting you changed in another both stay. Each window keeps showing its own layout; a new window opens the layout you used last. - **One window trades.** Paper and live trading, price alerts and syncing with your account run in one window only, so an order is never sent twice and an alert never rings twice. The other windows show **Second window** at the bottom; click **Trade here** to move trading to that window. Your paper account moves with it, exactly as it was. - **Closing or hiding the trading window.** When it closes (or crashes) another window takes over within a second or two. In the browser, a tab in the background is paused by the browser, so after a few seconds a visible tab takes over trading. TradingView, by comparison, lets one layout open in several tabs overwrite the others and recommends saving by hand in each tab. > [!NOTE] > Signed in, layouts sync to your account, and you can share one with other traders: **Layouts menu › Share this layout…**, and manage everything under **Layouts menu › Library…**. **Community layouts** in the same menu lists what others shared. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/layouts --- # Shortcuts > Every keyboard shortcut and mouse gesture in Flowscope. Chart handling follows TradingView, because your hands already know it. On macOS, read Ctrl as Cmd in the shortcuts below. > [!NOTE] > Global shortcuts are off while a dialog is open, a menu is open or a text field has the keyboard. Typing goes where you are looking. ## Keyboard ### Anywhere | Keys | Action | |---|---| | Ctrl+Z | Undo the last layout change | | Ctrl+Y | Redo | | Ctrl+Shift+Z | Redo | | Ctrl+S | Save the layout | | Ctrl+K or Ctrl+Shift+P | Command palette: find any action by name | | Ctrl+1 … Ctrl+9 | Open saved layout 1 to 9, in the order of the layouts menu | | / | Open the indicator dialog | | A–Z | Start typing a ticker: symbol search opens with what you typed | | Esc | Cancel the drawing in progress and go back to the crosshair | Type-to-search reacts to letters only, and not while Ctrl or Alt is held. ### Your own keys **Settings › Keyboard shortcuts** lists the actions whose keys you can change: the command palette, save, undo and redo, new window, the order ticket, the account panel, the AI assistant, bar replay, the screener, a screenshot, a new alert, chart settings and every drawing tool. Click an action's keys and press the new ones (with Ctrl, Alt and Shift as you like); Backspace leaves the action without keys, Esc keeps the old ones. Keys taken from another action leave it, and a notice says which. **Reset** brings back the default. Your keys sync with your other preferences. ### Command palette Ctrl+K (or **Command palette** in the Flowscope menu) lists every action by name: dialogs and panels, adding any pane, the presets, your saved layouts, every drawing tool and the settings, each with its shortcut where it has one. Type a few letters of any words in it (`add foot`, `fib`, `replay`); ↑ ↓ choose, Enter runs, Esc or a click outside closes. ### Pointer over a chart | Keys | Action | |---|---| | Alt+R | Reset the chart view | | Delete or Backspace | Remove the selected drawing | | Esc | Cancel placing a drawing, clear the selection and leave the drawing tool | | Shift+→ | [Bar replay](/docs/app/replay): show the next bar | | Shift+↓ | Bar replay: play or pause | ### In dialogs | Keys | Where | Action | |---|---|---| | ↑ ↓ | Symbol search | Move the highlight | | Enter | Symbol search | Open the highlighted market | | Enter | Name and date prompts | Confirm | | Enter | Price grouping, custom step | Apply the step | ## Mouse on a chart ### Plot area | Gesture | Action | |---|---| | Drag | Pan in time. Dragging vertically by more than a few pixels leaves auto scale and pans prices too. | | Flick and release | Keeps gliding and slows down | | Wheel | Zoom time with the right edge fixed | | Shift+wheel | Pan in time | | Horizontal scroll (trackpad) | Pan in time | | Pinch, or Ctrl+wheel | Zoom time around the pointer | | Shift+drag | Measure price and time distance | | Right-click | Context menu: reset view, auto scale, log scale, add indicator, chart settings, remove drawings | | `»` button (bottom right) | Scroll back to the most recent bar | When you scroll back to the newest bar, the chart re-attaches to the live edge and follows new bars again. ### Axes | Gesture | Where | Action | |---|---|---| | Drag | Price axis | Stretch or compress prices | | Wheel | Price axis | Stretch or compress prices | | Double-click | Price axis | Back to auto scale | | Drag | Time axis | Stretch or compress time | | Double-click | Time axis | Default bar width, back to the live edge | | Drag or wheel | Study pane value axis | Stretch that pane's scale | | Double-click | Study pane value axis | Fit the pane to its data again | ### Panes inside a chart | Gesture | Action | |---|---| | Drag a separator | Change the height of the price pane and study panes | | Hover a legend row | Show the eye, settings and remove buttons | | Double-click a legend row | Open the study's settings | ### Drawings | Gesture | Action | |---|---| | Click with a drawing tool | Place the first anchor; one-click tools finish here | | Click again | Place the next anchor | | Drag with a drawing tool | Place the first anchor at the press and the next where you let go | | Drag with the brush or highlighter | Draw freehand | | Alt+T, Alt+H, Alt+F … | Pick a drawing tool (see [Drawings](/docs/app/drawings#keyboard-shortcuts)) | | Click a drawing | Select it and show its style bar | | Drag a drawing's body | Move it | | Drag a handle | Move that anchor; snaps to open, high, low or close when the magnet is on | | Double-click a drawing | Select it | | Drag a box with **Zoom in** | Zoom to the box | Locked drawings, and every drawing while **Lock all drawings** is on, cannot be dragged. ### Trading | Key or gesture | Action | |---|---| | Alt+O | Order ticket and account panel | | Alt+P | Account panel: positions, orders, history | | Drag an order line or its label | Move the order | | Drag **TP** or **SL** out of the position label | Set a take profit or stop loss | | **×** on a line's label | Cancel the order or close the position | | Click the armed ladder | Limit or stop at that price (left buys, right sells) | See [Trading and paper accounts](/docs/app/trading). ## Mouse on other panes | Gesture | Where | Action | |---|---|---| | Double-click the free header space | Any pane | Maximize or restore the pane | | Click the link chip | Any pane | Join link group 1–5 or unlink | | Click the interval chip | Chart header | Open the interval menu for that chart | | Drag a border | Between panes | Resize panes | | Wheel | Ladder, depth book | Scroll rows (three per notch) and turn off auto-centre | | Wheel over the `PG` chip | Ladder, depth book | Step the price grouping | | Click the crosshair chip | Ladder, depth book | Centre on the last price | ## Menus In menus with stars (intervals, chart types, templates, layouts), clicking the star toggles the favourite without closing the menu. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/shortcuts --- # Liquidity heatmap > How Flowscope records resting liquidity and how to read the heatmap, bubbles and book profile. The liquidity heatmap shows where limit orders rested over time. Each price level is a thin horizontal line whose brightness is the resting size. Walls glow, thin liquidity fades, and you can see orders arrive, sit and get pulled or filled. ## How it is recorded Flowscope keeps the exact order book of every open market live: a snapshot plus every change from the venue's websocket, with the venue's sequence checks. From that book it records **each price bucket's resting size at the close of every minute**, stored as runs: ```text (start, duration, price bucket, quantity, side) ``` - A price bucket is about 0.001 % of the price. - A run is cut only where a bucket's size at one minute's close differs from the minute before, so a level that rests for hours is one run, not a snapshot per minute. - The minute that is forming shows the book as it stands; once it closes, it keeps what stood at its close. - A gap in the venue's sequence, or a dropped connection, ends every run at that instant, clears the book and resynchronizes. Nothing is invented to fill the gap. What this gives up, by choice, is whatever happens inside a minute: an order that came and went within it, such as the one that sat for 400 ms or the wall pulled and re-posted, is not in the record. On bars shorter than a minute the heatmap moves in minute steps. How far above and below the price the heatmap reaches is how deep each venue publishes its book. For BTC (measured October 2026): | Venue | How the book is built | Reach around the price | |---|---|---| | Coinbase | The whole book on the stream, then every change | everything: cents to millions | | Deribit | The whole book on the stream, then changes chained by id; inverse amounts in BTC | everything: $6 700 to $300 000 for BTC | | Deribit Spot | The whole book on the stream, then changes chained by id (`BTC_USDT`: about 50 levels a side) | everything | | Binance spot | REST snapshot (5 000 levels) and 100 ms depth deltas | about ±1 % at first, wider as the price moves | | Binance Futures | REST snapshot (1 000 levels) and 100 ms depth deltas | about ±0.2 % | | Bybit | 1 000-level snapshot and deltas on the stream | about ±0.2 % | | Bybit Spot | 1 000-level snapshot and deltas on the stream | about ±0.8 % | | OKX Spot | 400-level snapshot and deltas on the stream | about ±0.2 % | | Kraken | 1 000-level snapshot and updates on the stream; levels that leave its depth are dropped here | about ±3 % | | OKX | 400-level snapshot and deltas on the stream, quantities scaled by contract value | about ±0.06 % | | Aster | REST snapshot (1 000 levels) and 100 ms depth deltas | about ±2 % | | Lighter | The whole book on subscription, then updates | the whole book | | Hyperliquid | Full book images every block, diffed against the previous one | 20 levels a side | An aggregated market (**All venues**) sums them, so its heatmap reaches as far as the deepest venue in it. Coinbase is one of the venues BTC, ETH and the other majors sum by default, so walls thousands of dollars away show. ## Adding it to a chart The heatmap is the **Liquidity Heatmap** study. It is already on liquidity charts and the included [Liquidity & execution layout](/docs/app/layouts#starting-a-layout). Add it to any chart from the indicator dialog (/), group **Heatmaps**. The **Liquidity (price line)** chart type hides the bars entirely and leaves the heatmap and a price line. It is the cleanest way to read liquidity. ## Settings Automatic filtering is always active for the order-book heatmap. It suppresses short presence streaks at the current time scale, keeps continuous levels through size changes, and shows new live orders immediately. It measures the book's resting liquidity in view and shows its whole spectrum: only the dust (single lots, a fraction of a percent of what the book holds) is left out; thin depth is dark violet, ordinary depth blue to green, walls yellow, on a logarithmic ramp that adapts to the market. The book is one continuous field, as on Market Monkey: thin depth is dark navy rather than empty, and only where no book was recorded shows the chart's background. There are no quick-setup levels to choose. **Reset automatic** clears manual thresholds, contrast and range overrides while keeping your markets, colours and opacity. The chart's range slider remains available for fine-tuning. The order book is drawn in cells, as on Market Monkey: each column is one bar, each row a fixed price band. **Resolution** picks the rows as Market Monkey does, from its table per coin: **HD** ($5 on BTC, 0.25 on ETH, 0.02 on SOL, 0.05 on BNB, 0.0002 on XRP) or **SD** (five times coarser: $25 on BTC); **Custom** uses the row size set on Advanced (0: about 0.012 % of the price on a round step, $10 on BTC). Each cell is the book at the bar's close, as Market Monkey's. Zoom in far enough and each cell shows its size. The liquidation heatmap and the Hyperliquid maps are a sharp image at the screen's own resolution, each recorded price bucket its own line. The order book and the Hyperliquid maps lay their settings out as Market Monkey's: **General** (resolution, the profile, then the colormap and the intensity block), **Interaction** (cell values, units, tooltip, toolbar, extension) and Flowscope's **Advanced**. The liquidation heatmap has Market Monkey's **General**, **Style** and **Leverage Bands**, then Advanced. | Setting | What it does | Default | |---|---|---| | Markets | The markets the heatmap reads: the chart's, another market of its coin, or several summed. See [Markets of an indicator](#markets) | The chart's | | Resolution | SD, HD (Market Monkey's rows per coin) or Custom (the row size on Advanced); the order book | HD | | Show profile, Profile outline | The levels resting now as a profile at the right edge in the heatmap's colours, and each side's running sum from the price as its outline | On, on | | Profile width, Profile scale | The profile's width as a share of the chart (0 to 0.5) and how far its bars reach (0.25× to 4×). On the Hyperliquid maps the rows share the outline's scale (the larger side's running sum), as Market Monkey's, so they stay short beside it | 0.15, 1.00× | | Colormap | 49 ramps: first Market Monkey's 27 colormaps in its order, with its names and its colours as its colormap bar shows them (Deep, Dark, Pastel, Paired, Viridis, Plasma, Hot, Cool, Pink, Jet, Twilight, RdBu, BrBG, PiYG, Spectral, Greys; MMT Royal Mint, Emberglow, Deep Ocean, Neon Noir, Solarflare, Earthen, Cyberpunk Ice, Vintage Chart, Spring Blossom, Acid Sunset and Liquidations), then Flowscope's own: Ocean, Thermal, Mono, Magma, Inferno, Turbo, Bookmap, Cividis, Mako, Rocket, Ice, Cubehelix, Abyss, Parula, Rainbow, Spectral reversed, Coolwarm, Copper, Bone, Blackbody, Electric and Sunset | Viridis | | Intensity range | Two handles over the colormap: sizes below the left one are not drawn, colours saturate at the right one; the arrow returns to the whole range | 0 to 1.0 | | Opacity | How strongly the heatmap covers the chart, in steps of 0.01 | 0.85 | | Gamma | Gamma of the mapping, 0.1 to 10 | 1.0 | | Minimum, Maximum | A fixed range instead of the measured one, as Market Monkey's: nothing below the minimum is drawn, the maximum is full colour, between them its logarithmic intensity, ln(1 + size in the coin); in the unit sizes are shown in. Maximum 0 measures the range in view | Measured | | Reset intensity range | The whole range, Minimum and Maximum back to the measured scale | | | Cell values, Compact | The size printed in each cell large enough for it, short (1.2K) or in full | On, on | | Value units | Follow global (the app's unit, Settings › General: "Follow global (Coin)"), Coin or USD | Follow global | | Zoom tooltip, Lens grid | The lens: a table of the sizes around the pointer (one bar by one price bucket each) with each one's time, band, size and bid share; 3 × 3, 5 × 5 or 7 × 7 cells | On, 3 × 3 | | Quick toolbar | The layer's row in the floating heat-layers panel | Off | | Extend latest | Levels resting now drawn past the last bar, updated as the book changes | Off | | Extension mode, Extension bars | To the plot's right edge (Viewport) or a number of bars past the newest (Bars) | Viewport, 1 | | Price scale | What the chart's automatic price axis spans: **Heatmap depth**, the band holding nearly all (99 %) of the resting liquidity recorded in the time in view, up to ±25 % from the price; or **Candles**, the candles alone | Candles | | Saturation percentile (Advanced) | Where full colour sits in the resting liquidity in view when no Maximum is set (0.995: its 98th percentile; 1: the largest) | 0.995 | | Market Monkey's intensity from zero (Advanced) | The measured scale from zero on Market Monkey's curve, ln(1 + size in the coin), then the gamma, instead of the measured spectrum | On | | Low values drawn solid (Advanced) | The ramp opaque from its first colour, as Market Monkey draws heatmaps, instead of fading the thinnest sizes in | On | | Cells at the bar's close (Advanced) | Each cell the book as the bar closed (Market Monkey's), else the mean over the bar | On | | Profile in the heatmap's colours (Advanced) | The book profile's rows in the heatmap's colours (Market Monkey's), else in fixed bid and ask colours | On | | Bids and asks in their own colours (Advanced) | Bids in the bullish hue, asks in the bearish one, brighter with size | Off | | Label the largest walls, Mark pulled and filled large orders (Advanced) | The largest levels resting now in view, labelled at the right edge; what became of large orders that left the book | Off | All heat layers (the order book, the liquidation heatmap and the Hyperliquid maps) share these settings and their defaults (Market Monkey's look); a layer adds its own after them (the liquidation heatmap its profile and leverage bands, the book its resolution, walls and price scale). The range sliders are the quickest control (each layer's Quick toolbar, off by default as on Market Monkey; its settings have the same Intensity range on General). The heat layers with it on share one small chip at its top right showing their colour ramps. Hover it (tap it on a touch screen) and it opens a panel with one row per layer: its name and source, its range slider, and buttons to reset the range, open the layer's settings, or hide its row. The panel stays open while the pointer is in it and folds a moment after it leaves (on touch, tap outside it). Drag the left handle right to dim thin liquidity; drag the right handle left to saturate walls sooner. Changes apply live. The chip appears when the chart is larger than about 380 × 160 px; drag the panel's grip to move it. ### Walls and order fates {#walls} The largest levels resting now in view are labelled at the plot's right edge, beside the order book profile: their notional, their size and how long they have rested without a break (`$13.4M · 204 · 1m`). Hover a label for its price band. A label stays with its wall while it holds about the size of the newcomers, so labels do not jump between walls of the same size. When a large order leaves the book, the heatmap marks what became of it: a **filled** order (trades at its price took at least half of it) is a white dot, a **pulled** one (it went without trades) an orange ring struck through. Only the largest drops of the market count (the top 1 % of recent ones). The marks come from the live book since the chart opened: the recorded history keeps the book once a minute and cannot tell a fill from a cancel. ### How a level is drawn {#lines} Each price bucket is drawn as it rests: a thin three-pixel line where the bucket is taller than three pixels on screen, its own pixels where it is smaller, so a zoom never makes a line jump in width. Finer drawing does not recover events absent from the recorded history; the lens shows exact recorded quantities. ### The scale {#scale} Intensity is relative to what is on screen and logarithmic, so depth that differs by orders of magnitude stays apart: thin depth dark, walls bright. Both ends are measured on the book's resting liquidity in view (size × time), so a deep book's many tiny levels do not drag the scale down. A pan, up, down or sideways, never changes the scale or the legend's `≥ size`; it is measured again only when the settings, the market or the timeframe change, or the view is zoomed by more than a factor of two. Zooming into a quiet area therefore still brings out structure that a busy area would drown. The price axis follows the same idea: with **Price scale** on *Heatmap depth* it spans the depth recorded in view, measured every few seconds and when the view has moved on, and a new measure close to the current one keeps the axis where it is. Dragging the price axis still takes over by hand. Double-clicking it (or the **auto** button under it) returns to automatic scaling over the candles and sets **Price scale** to *Candles*; right-click the price axis and choose **Fit heatmap depth** to span the depth again. ## Smooth panning Rasterizing runs on an engine worker, never on the interface thread. A raster reaches past the plot on every side and is drawn again only when the view leaves that margin, the zoom changes or the book does. A pan inside that margin only moves the image. While a new image is on its way, the chart keeps drawing the last one mapped into the current view, so panning and zooming stay smooth. Changing the timeframe keeps the heatmap where it is in time: it does not depend on the candle interval and is drawn again on the new bars as they load. When only the book changed, only the live part is drawn again: the columns from the minute that was forming at the last image on. The history in view stays as it was drawn, as a minute's levels are its close's. That takes about a millisecond where a whole image takes a hundred, so the heatmap follows the book live even in the browser. Every five seconds, and whenever history from the hub arrives, the image is drawn whole once. Zoomed far out, the hub sends its history at a coarser time resolution (down to one slot an hour), so a week in view loads and draws far fewer levels. ## Trade bubbles The **Trade Bubbles** study draws the largest market orders in view as circles sized by quantity, buys and sells in their own colours. | Setting | Default | |---|---| | Bubbles in view | 60 (5 to 400) | | Hide below (fraction of the largest) | 0.06 | | Size | 1.0 | | Fill opacity | By the bubble's size (0); or one opacity for all | | Outline thickness | 1 px; 0 draws none | ## Order book profile The **Order Book Profile** study draws the current book as horizontal bars at the right edge of the chart, bids and asks in their own colours. Its width defaults to 96 px. ## Aggregated heatmaps On an [aggregated market](/docs/app/aggregated-markets) the heatmap is drawn from the summed book of its venues. Each venue's contribution is tracked separately, so a venue that resynchronizes or disconnects removes exactly what it added. ### Markets of an indicator {#markets} The heatmap does not have to read the chart's market. **Markets** in its settings opens the same picker as a pane's: the coin's venues in a perpetuals, a coin-margined and a spot column, each with **All**, the chart's market marked `current`. Choose one market to see its heatmap on this chart, or several to see them summed: an aggregated heatmap of Binance, Bybit and OKX over a Binance chart. **Chart's market** goes back to the chart's own. The legend names the choice (`Heatmap Viridis · 3 markets`). The Liquidation Heatmap, Trade Bubbles, the Order Book Profile and Liquidations have the same setting, and so do the indicators computed from bars: CVD, Delta Volume, Volume, Trade Counter, Open Interest, Net Positioning, OB Depth and Liquidation Volume. Those run on the bars of their markets at the chart's interval and land on the chart's bars by time (a CVD of its own markets draws their sum, not one line per venue). [Bar Stats](/docs/app/bar-stats) reads every row from its markets' bars the same way, and the [visible-range volume profile](/docs/app/profiles#vpvr) is built from their footprints over the time on screen. The footprint chart itself shows the chart's market: open an [aggregated market](/docs/app/aggregated-markets) for a footprint of several venues. Each indicator keeps its own choice. When the chart turns to another coin, the same venues are taken for it, and venues that do not list the coin are left out. The history of a summed choice is summed too. The hub records every venue's heatmap; for a choice of venues it sums their recorded hours when the app asks for them, so the aggregated heatmap reaches back as far as its venues were recorded. It reads the venues at once and keeps each sum for everyone who asks for the same hours next; choosing exactly the usual venues of the coin's aggregate reads that aggregate's recorded hours instead. Changing the choice draws the heatmap again from the new sum. ## History No venue publishes historical order books: heatmap history is what the archive holds, the book once a minute. The hub serves Derive's tiles for the hours before you opened the market, back as far as the market was captured ([Data](/docs/data)). The app keeps no heatmap on the machine: each session loads it from the hub again. > [!NOTE] > Serving sealed heatmap hours from object storage is planned, as is a level-of-detail pyramid for very long windows. Today the hub serves the tiles from its own disk. ## Related layers - [Liquidation heatmaps](/docs/app/liquidations#liquidation-heatmap) use the same raster with their own ramps: modelled liquidation levels, and measured Hyperliquid take-profits, stops and liquidations. - The [depth book](/docs/app/dom-and-tape#depth-book) shows the current book as a list with cumulative depth. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/heatmap --- # Footprint > Bid × ask, delta and volume per price inside every bar, with imbalances, POCs and history on demand. A footprint opens each bar up and shows what traded at every price inside it: how much hit the bid (sellers) and how much lifted the ask (buyers). Flowscope draws footprints over the whole loaded history, not just the live session. ## Open a footprint - Pick **Footprint** in the chart type menu, or - add a footprint pane from **Add pane → Footprint**, or - start from the **Orderflow desk** preset, which opens a 15-minute footprint of BTC on all venues. A new footprint chart comes with its own naked POCs, a visible-range volume profile, VWAP, CVD and Bar Stats. The default bar width is 84 px so the cells have room for numbers; each column leaves a gap to the next (**Column gap**, 20 % of the bar spacing), and the cells use the rest, so wider bars mean wider, more readable cells. ## Display and settings {#settings} Hover **Footprint Standard** in the chart's upper-left legend to show its eye, settings and remove controls. The eye hides the footprint; remove returns the chart to candles. **Footprint settings…** is also available from the chart type menu. **Cluster** draws equal-width cells. **Profile** draws horizontal bars proportional to traded size. Both keep sell numbers on the left, buy numbers on the right, and a narrow candle between them. **Mode** selects Standard (sell/buy), Delta or Volume; Volume shades every cell in one grey ramp whose brightness follows the row's volume (the colour is **Volume** under Colors). Numbers appear when the row is tall and wide enough to read, in dark or light ink to contrast with the cell. A number that would not fit its cell is left out rather than clipped, rows that round to zero show no number, and a column cut by the chart's edge keeps its cells but not its numbers. The settings apply live and are saved with the chart. **OK**, the close button and Escape keep changes; **Cancel** restores the opening state. **Template** saves every footprint section, including the Aggregate markets and Global ticks per row. The selected template is remembered in the layout and checked in the menu; later changes show **modified**. Right-click a saved template to delete it. With **Gradient** off (the default, as Market Monkey's), qualifying cells use the selected color at full strength. With **Hide filtered** off, rows below the size filter remain neutral grey and keep their numbers when readable. **Skip grade filtered** removes those rows from size grading; **Hide text** independently hides numbers. Templates created before aggregation was included remain usable and keep the current data source. Save them again to include markets and the Global row value; those missing values cannot be recovered from an old template. | Section | Controls | |---|---| | Price Marker | Independent axis label, inline label, price line, countdown and line width | | Display | Cluster/Profile; Standard/Delta/Volume; text content and visibility; column gap; rounding; gradient; global, coin or USD units; minimum volume; exclude filtered rows from grading; hide filtered rows | | Header | Total, sell, buy and delta beneath each bar, with optional backing | | Row / Scaling | Auto/Global/Custom row size, ATR length, and shading relative to a candle, visible range, custom lookback, day, week or loaded chart | | Profile | Additional volume or delta profile per bar, with independent colours: with the Profile display behind the bar's own rows, its column wide (a delta profile over the volume's); with Cluster beside the bar | | POC | Colour, solid/dashed/dotted line, naked extension, outline and thickness | | Value Area | Dotted upper/lower boundaries and arrows, colour and volume percentage | | Imbalance | Buy/sell colours, skip zero comparisons, outlines and line style, coloured text, ratio, minimum volume, minimum contiguous stack and naked extension | | Summary | Combined column, own display/mode or follow footprint, visible range or last N loaded bars, offset | | Colors | Up/down, volume and POC colours | | Candles | Visibility, up/down colours, centered or left position, and Market Monkey's **Candle layout**: Standard (a filled candle), Bar (a high-low line with the open's tick left and the close's right) or Hollow (up candles as an outline, at every zoom) | | Aggregate | Markets whose trades contribute to the footprint | New footprints start as Market Monkey's: rounded numbers, cells graded relative to their candle without a gradient, its up and down colours (`#4ED78E`, `#ED8172`) and green volume (`#82F88C`), a 20 % column gap, centered Standard candles, a boxed header when shown; the POC, naked POCs, value area, extra profile, summary and imbalance marks start off. The imbalance ratio starts at 10 and zero comparisons are skipped. Charts saved before keep their own settings: **Defaults** in Footprint settings switches one to the current look (then pick the mode again). ## Imbalances Standard mode compares diagonally: buys at one row against sells one row below, and sells against buys one row above. The ratio is a multiplier, so 10 means ten times the opposite volume. **Stacked imbalances** requires consecutive qualifying price rows; a missing price row breaks the stack. **Naked extension** stops when a later candle trades through the marked price, including a gap across it. Scaling, anchored summaries and naked extensions use loaded footprint history. Missing historical footprint data is not invented. ## Naked POCs With **Show POC** and **Naked POC** (both on for new footprints), each bar's point of control gets a small **POC** label beside its column and a dotted line from the column's right edge to the right, for as long as price has not traded through it since. Untested POCs are levels many traders watch. The **Naked POCs** study draws the same kind of line on any chart. On a footprint whose own naked POCs are on it steps aside; otherwise its lines, like the visible-range profile's POC ray, run behind the cells and show in the gaps between columns instead of striking through numbers. ## Rows {#rows} Footprints are stored per minute in rows of about 0.005 % of the price. How many of those a cell holds is the chart's **Footprint rows** setting, in the chart type menu (under Footprint) and in **Footprint Settings → Row / Scaling**: | Footprint rows | Cells | |---|---| | Auto (default) | Merged so cells stay at least 14 px tall at the zoom you are using | | 1, 2, 5, 10, 25, 50, 100, 250 or 1000 ticks, or any number | That many ticks of the market's price per cell, at any zoom: larger rows read without zooming in | | Global | The market's own value, **Global ticks per row** in the same section (0: automatic), shared with every profile set to Global | A cell always holds whole stored rows, so a value finer than the stored row rounds up to one. Stacked imbalances and cluster search read the same cells. Sub-minute, tick, volume and notional bars are built from the raw tape and use the same cells. With the row size on **Auto**, **ATR length** in the footprint settings makes rows from volatility instead of the zoom: each row is about a tenth of the average true range of that many bars, rounded up to a round number of ticks, so cells keep their price span when you zoom. 0 (default) keeps rows about 14 px tall at any zoom. ## Where footprint history comes from Footprint history is what the archive holds ([Data](/docs/data)). Charts ask for the footprints of the range you are looking at, and the market loads them whole from the data hub, one request per day, newest day first. The browser app reaches the hub through the website at `/hub`. See [History and data](/docs/app/history-and-data#hub). Footprints are never rebuilt from a venue's trade history, and bars are never taken from a venue's candles: before the hub's recording begins, a chart has no bars. > [!TIP] > `make dev` runs a data hub beside the app that records BTC and ETH on every venue by default. On a server, the hub records around the clock, so every chart opens with its footprints whole. ## Minute states Every minute, and so every bar, carries a state: | State | Meaning | |---|---| | Full | Every print is known | | Partial | Some prints: the session started mid-minute, a connection dropped, or a venue of an aggregate is missing | | Loading | Being fetched | | Unavailable | Nobody recorded it | | None | Nothing known and nothing asked for | Bars without footprint data show the hub's candles, built from the same recording. While footprints of the visible range are still loading, the chart says how many bars are done. ## On aggregated markets The footprint of an [aggregated market](/docs/app/aggregated-markets) is the sum of the venues' minutes. History is `Partial` where one of its venues was not recorded. ## Hyperliquid positioning footprints Three more footprint studies draw what Hyperliquid trades did to positions, per bar and price: **Footprint Hyperliquid OI**, **NL** (net longs) and **NS** (net shorts). See [Liquidations](/docs/app/liquidations#hyperliquid-positioning-footprints). Source: https://flowscope.91-99-118-129.sslip.io/docs/app/footprint --- # DOM and tape > The order-flow ladder, the depth book and time & sales. Three panes show the order book and the tape as lists rather than charts. Add them from **Add pane** in the top bar: **Order book (ladder)**, **Order book (depth)** and **Time & sales**. The **Orderflow desk** preset opens all three. ## Ladder The ladder ("DOM") puts the price in the middle. Around it are the resting book, what traded at each price and the most recent market orders. | Column | Shows | Optional | |---|---|---| | Sell, Buy | Market sells and buys of the last few seconds at each price | Yes | | Bids, Asks | Resting size on each side | No | | Pulling / stacking | How the resting size changed against a few seconds ago, next to each book side | Yes | | Price | The price row | No | | Delta | Traded buy minus sell volume at the price | Yes | | Volume | Traded volume at the price, with the point of control | Yes | | B.T, A.T | Volume sold at the bid and bought at the ask at each price, over the volume window; beside the price | Yes, off by default | | VP (volume profile) | Traded volume per price over the volume window at the ladder's right edge: one bar per row growing from the right, its size inside, the point of control in red, the 70 % value area brighter | Yes, off by default | ### Ladder settings Open them with the gear in the pane's toolbar. | Setting | Default | |---|---| | Columns: delta, volume, pulling / stacking, trades | All on | | Pulling and stacking compare with | 10 seconds ago | | Sell and Buy columns sum (Prints window) | The trade retention, as Market Monkey's one window (so Sell + Buy is a row's Volume), or a window of their own from 1 second to 12 hours | | Volume and delta reset, Trade retention (also B.T, A.T and the profile) | Session (00:00 UTC), or a rolling window from 1 minute to 12 hours | | Volume markers | POC, VAH, VAL and the VWAP of the trades held, marked in the Volume column, each in its colour (POC amber, VAH and VAL blue, VWAP violet; Advanced › colours) | | B.T / A.T columns, volume profile column | Off | | Profile value area brighter | On | | Cumulative depth behind the book columns | Off | | Book heat colours: Bid and Ask cells coloured by size on a heatmap ramp (Bookmap, Thermal, Viridis…) instead of size bars | Off | | Auto (drift to edge): recentre only near the top or bottom | Off | | Recenter at: an edge offset in rows, or 10, 25 or 33 % of the ladder from the edge | 5 rows | | Row height | 16 points | | Number format | Compact (`950`, `1.2K`, `4.1M`), full (`1,234.5`) or two decimals as Market Monkey's (`13.61`, `0.05`, `1.23K`) | | Font size | The dense monospace size | ## Depth book The depth book lists asks above and bids below the spread. - The **cumulative depth** is drawn behind the prices as a step curve. - A **delta** column shows how each level's size changed over the last few seconds. - **Markers** flag unusually large orders. - A **Sum** column adds the sizes up from the spread outwards, each with a bar against the largest sum in view; **size bars** do the same for each level's size. - A thin **trade heat strip** at the right edge colours each price by what traded there over the last minute (Thermal ramp, brightest where most traded), so you see where the market has been dealing against the resting book. When the pane gets narrow, the Sum column goes first, then Delta, then the markers. | Setting | Default | |---|---| | Delta compares with | 10 seconds ago | | Mark levels at least | 4 × the average visible size | | Markers, curve, delta column | All on | | Sum column, size bars, trade heat strip | All on | | Trade heat window | 60 seconds | | Row height, number format, font size | As the ladder | ## Toolbar The ladder and the depth book share one toolbar: | Control | What it does | |---|---| | `PG` chip | Price grouping. Click for presets (Auto, or 1, 2, 5, 10, 25, 50, 100, 250, 500, 1 000 ticks) and a custom step; scroll over it to step through presets. | | Crosshair chip | Centre on the last price | | `A` | Auto-centre: keep the last price (ladder) or the spread (book) in the middle | | ↺ (ladder) | Reset the traded columns: Delta, Volume and the Sell / Buy prints count from now, until the next session starts (or the rolling window has moved past the reset) | | Coin / USD | Show quantities in the base coin or as quote notional | | Venue mark | The market's venue (shown when the pane is wide enough) | | Gear | The pane's settings | Scrolling the rows with the wheel turns auto-centre off. The crosshair chip or `A` brings the price back. > [!NOTE] > The ladder shows a **Trade** chip on wide panes. Trading is not available yet; the chip does nothing. ## Time & sales {#time-and-sales} Time & sales lists prints newest first with time, price and size. Large prints are highlighted: a print counts as large when it is at or above the 97th percentile of the last 2 000 prints. On an [aggregated market](/docs/app/aggregated-markets) the tape is the merged tape of its venues. Each live print shows its venue's logo (prints loaded from history do not say where they traded), and prints of different venues at the same time stay separate rows when orders are combined. | Setting | Default | |---|---| | Combine orders: one row per taker order | On | | Hide below | 0 USD | | Highlight from | Auto (the largest 3 % of recent prints) | | Spot venues, perpetual venues (aggregated markets) | Both on | | Trade sounds from: a short click per print from this notional, higher for buys, lower for sells, at most every 80 ms | Off | | Size bars | On | | Size grading: rows shaded in four steps from a tenth of the highlight size to the highlight size, instead of size bars | Off | | Venue logos (aggregated markets) | On | ## Where the data comes from All three panes read the market's live state. Everything that needs history, like volume at price or the earlier book for pulling and stacking, is kept incrementally while the pane is open, so a frame touches only the visible rows and the new prints. When a market opens, Flowscope seeds the tape with the data hub's last 30 minutes of trades, so the time and sales is not empty. The tape holds what the hub recorded, never a venue's trade pages. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/dom-and-tape --- # Profiles > Volume profiles for the visible range and per session, and TPO Market Profile. Profiles turn time sideways: they show how much traded, or how long price spent, at each price. Flowscope has three, all in the **Profiles** group of the indicator dialog. | Study | Short name | Built over | |---|---|---| | Volume Profile (Visible Range) | VPVR | The bars on screen | | Session Volume Profile | VPSV | Each session | | TPO Profile (Market Profile) | TPO | Each session, in fixed periods | Every profile marks the **point of control** (POC, the price with the most volume or time) and the **value area** (the range around the POC holding a chosen share of the total, 70 % by default) with its high (VAH) and low (VAL). ## Settings Every profile's settings open from the cog on its legend row, in tabs: **Display**, **Row / Scaling**, **POC**, **Value Area** (and **Session** for session profiles), then **Style** for colours, widths and line styles, and **Aggregate** for its markets. The POC, VAH and VAL lines take a solid, dashed or dotted style in the Style tab. ## Visible-range volume profile {#vpvr} Traded volume at each price for the bars currently visible. It updates as you pan and zoom. | Setting | Default | |---|---| | Rows | 24 (0 for automatic, or 1 to 2 000) | | Value area % | 70 | | Width % of the chart | 30 | | Place on the right | On | | Volume colours | Up / down, total, delta, total + delta, or **Outline** (only the profile's outline) | | Colour grading | Rows brighter the more they traded | Off | | Threshold filter | Market Monkey's filter: rows smaller than this share of the largest one (in percent) drawn in the threshold colour (grey by default), the larger ones in their own; 0 is off. The session profile and the TPO's volume profile read it the same way | 0 | | Histogram box | The profile's area outlined | Off | | Highlight the value area | On | | POC / VAH / VAL labels | On | | Extend POC across the chart | On | | Row values | Off | **Markets** builds the profile from another market of the coin, or several summed, over the time on screen (the legend says `VPVR 70 · Bybit`). See [Markets of an indicator](/docs/app/heatmap#markets). **Row values** writes each row's volume (its delta with delta colours) on the row when there is at least 15 px of drawing height. With automatic rows, the profile reserves 17 px including spacing and rounding so the numbers fit. Threshold highlighting changes the colour while preserving the selected quantity, bar length and value label. ## Session volume profile {#vpsv} One volume profile per session, drawn inside the session's bars, with **developing** POC, VAH and VAL lines that show how the levels moved while the session formed. | Setting | Default | |---|---| | Session | Daily | | Value area % | 70 | | Row size | Auto, Global or Custom ticks per row (see [Rows](#rows)) | | Width % of the session | 70 | | Volume colours | Up / down | | Highlight the value area | On | | POC label, volume label, delta label | On | | VAH / VAL labels | Off | | Colour grading, threshold filter | As the visible-range profile | | Extend naked POCs | Off | | Extend naked VAH / VAL | Off | | Naked levels on the price axis | Off | | Row values | Off | | Session background | Off | | Session header | Off: the session's POC, VAH, VAL, volume and delta above it (Session tab), on a backing unless **Header without a backing** | ### Naked levels A finished session's POC, VAH or VAL is **naked** until a later bar trades through it. A gap past the level counts, the forming bar does not. **Extend naked POCs** and **Extend naked VAH / VAL** run those lines on to the right edge of the chart while they are naked, and stop them at the bar that traded through. **Naked levels on the price axis** tags each naked level of the sessions in view on the scale, in its line's colour. The developing POC is shown by default; the developing VAH and VAL start hidden. Turn them on in the study's **Style** tab; their line style (solid, dashed, dotted) is drawn as set, on the TPO's volume profile too. Value-area rows and the rows outside it always take their own colours (Value area rows, Outside VA rows), as on Market Monkey. Lines and labels take the colours set, unfaded: the VAH and VAL labels in the value area's colour. Every session profile and TPO on a chart is drawn, not only the first. ## TPO {#tpo} The TPO profile (Market Profile) assigns a letter to each period of a session and stacks the letters at every price the period traded. Wide rows are prices where the market spent time; thin rows are prices it moved through quickly. Use it in two ways: - as the **TPO (Market Profile)** chart type, which draws the profiles in place of bars, or - as the **TPO Profile** study over any chart. **TPO Settings** has twelve sections: General, Session, POC, Value Area, Poor High / Low, Single Prints, Tails, Markers, Price Marker, Price Source, Initial Balance and Volume Profile. The legend's settings control opens them directly. Templates preserve these options, colours and price markers. | Section | Controls | |---|---| | General | Auto/Global/Custom rows, letters and blocks independently, hidden text, flat/graded/spectrum colours, four spectrum stops, first/last-period colours, spacing, split all or only the latest session | | Session | Session period and custom hours/weekdays, block duration, session header and transparency | | POC | Show, inline/axis labels, naked extension, independent row/ray colours, thickness and line style | | Value Area | Percentage, profile/ray/fill colours, labels, naked extension, fill, outside-area fade, thickness and style | | Poor High / Low | Visibility, naked extension, ray colour, thickness and style | | Single Prints | Profile colour, fill extension and naked fill extension | | Tails | Visibility, naked extension, independent buy/sell minimum row counts and colours | | Markers | Independent midpoint, open and close markers with colours | | Price Marker | Last-price axis/inline labels, line, countdown and width | | Price Source | Optional line using period close, open, high, low, HL2 or HLC3 | | Initial Balance | Visibility, colour and number of periods | | Volume Profile | Side volume profile, up/down/volume/delta mode, origin, width, grading, values, background, inside/outside-area colours, threshold filter, POC and independently extended VAH/VAL | Each block column is at most as wide as its period on the time scale, as on Market Monkey: zoomed out, or with day blocks on a daily chart, the profiles are thin columns without letters. The default view draws letters on square blocks: blue value-area rows, grey outside rows, a pale POC and green single prints. The POC ray is solid; VAH/VAL rays are dotted. Value-area fill, initial balance, tails, poor extremes and the volume profile start off. Letters adapt to both row height and period-column width, between 7 and 13 px. They remain visible while they fit; smaller cells keep their blocks. In the TPO chart, changing the candle timeframe preserves session density, including daily and weekly source bars. The session period and block duration are separate settings. **Split by blocks** assigns a column to each period instead of compacting occupied letters to the left. **Last session** splits only the forming session. The selected block duration requests its own candle series when the chart interval is longer. Until those candles arrive, available chart candles provide a coarser profile; replay uses the replayed chart bars. Volume profiles use recorded footprint cells when available and distribute candle volume across its range otherwise. Naked POC, VA and poor-extreme rays stop at a later candle that trades or gaps across their price. Naked single-print and tail fills stop at the first later candle overlapping their price band. ## Rows {#rows} The visible-range and session profiles, the TPO and the Order Book Profile size their rows the same way: | Row size | Rows | |---|---| | Auto (default) | Sized for you: a share of the session's typical range (sessions, TPO), the zoom (visible range, book) | | Global | The market's **Global ticks per row**, shared by every layer set to Global and by the footprint. Set it from the **TPR** chip in the bottom bar (see below) or in the layer's settings | | Custom | **Ticks per row** of the layer's own | Larger rows group more prices into one, so a profile reads without zooming in. The **TPR** chip in the bottom bar shows the active chart's global value (`TPR: Auto` while it has none) and opens **Ticks per row**: the active chart's pair with **Set value** or its value, and every pair that has one. **Auto** beside a value drops it, and that pair's Global layers size their rows automatically again. Changes apply as you type; Cancel restores what was there. The command palette has it as **Ticks per row…**. ## Sessions Session profiles and TPO share one session setting. | Session | Hours | Clock | Days | |---|---|---|---| | Daily | 00:00–24:00 | UTC | Every day | | Weekly | The calendar week | UTC | — | | Monthly | The calendar month | UTC | — | | Asia | 09:00–15:30 | Tokyo (UTC+9, no daylight saving) | Mon–Fri | | London | 08:00–16:30 | London (GMT / BST) | Mon–Fri | | New York | 09:30–16:00 | New York (EST / EDT) | Mon–Fri | | Custom | Your start and end | UTC | The weekdays you tick | Exchange sessions follow the local clock including daylight saving time, so London opens at 07:00 UTC in summer and 08:00 UTC in winter. For a **Custom** session you set a start and end time in UTC (default 13:30–20:00) and tick the weekdays the session starts on. An end at or before the start wraps past midnight. The custom fields appear in the settings only when **Custom** is selected. ## How profiles stay fast A finished session is computed once and kept as long as its bars do not change. When history arrives, a cheap fingerprint of the bars decides whether to recompute. The forming session is extended from its newest bar a few times a second. Profile levels feed the legend, the data window and the price-axis tags like any other study value. ## Related - [Footprint](/docs/app/footprint) shows the same volume-at-price idea inside every bar. - [Naked POCs](/docs/app/footprint#naked-pocs) mark per-bar points of control that price has not revisited. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/profiles --- # CVD and delta > Delta volume, cumulative volume delta and trade counts, filtered by trade size and split by venue. Delta is buy volume minus sell volume, where a buy is a trade whose aggressor lifted the ask and a sell is one that hit the bid. It tells you who was pushing. Flowscope computes it per bar and as a running sum, and can restrict both to trades of a chosen size. ## Trade-size bands Every print is classified by its quote notional into one of 11 bands: | Band | Notional | |---|---| | 1 | under $1K | | 2 | $1K–10K | | 3 | $10K–25K | | 4 | $25K–50K | | 5 | $50K–100K | | 6 | $100K–250K | | 7 | $250K–500K | | 8 | $500K–1M | | 9 | $1M–2.5M | | 10 | $2.5M–5M | | 11 | $5M and up | Each bar and each stored minute keeps buy and sell volume and trade counts per band. Recorded history keeps the bands too, so you can filter delta by size over history, not only live. In a study's settings, pick bands with the chips or a quick preset: | Preset | Bands | |---|---| | All | Every band | | < $10K | Bands 1–2 | | ≥ $10K | Bands 3–11 | | ≥ $100K | Bands 6–11 | | ≥ $1M | Bands 9–11 | The legend shows the filter after the study name, for example `CVD ≥ $100K`. > [!NOTE] > Bars built from the data hub's candles, where no footprint minutes are loaded, and minutes saved before size bands existed, have no band data. With a band filter those bars show as missing, never as zero. ## Delta Volume **Delta Volume** draws buy minus sell volume per bar in its own pane, as a histogram or as candles. | Setting | Default | |---|---| | Trade sizes | All bands | | Values | **Follow global** (the app's unit, Settings › General: "Follow global (Coin)"), Coin or USD | | Style | **Candles** (Market Monkey's): each bar opens at zero and closes at its delta, its wicks at the highest and lowest delta reached inside the bar (all sizes, where every print of the bar is known); or **Histogram** | | Threshold, bars below it | 0 (off); bars below the threshold drawn in the threshold colour (Market Monkey's), or hidden | | Highlight \|Δ\| from | 0 (off): bars from this size on in the Highlight colour (Style tab) | | Gradient | Off: small deltas faint, the largest in view solid when on | New Delta, CVD, Open Interest and Liquidations panes start as Market Monkey's: its up and down colours (`#4ED78E`, `#ED8172`), sizes following the app's unit, no gradient; CVD a 2 px `#4D99FF` line, Open Interest a 2 px line whose **Series color** is Auto (the up colour) or Custom (its own colour on the Style tab). ## Cumulative Volume Delta **CVD** is the running sum of delta. A rising CVD with flat price means buyers are being absorbed; a falling CVD into support means sellers keep hitting it. | Setting | Options | Default | |---|---|---| | Trade sizes | Any set of bands | All | | Display | Line, candles or histogram | Line | | Reset | Never, or every session (1D, UTC) | Never | | Values | Follow global, Coin or USD | Follow global | | Per-venue lines | On aggregated markets, one extra line per venue | Off | In **histogram** mode each bar shows its own delta, not the running sum: buying above zero in green, selling below in red. In **candles** mode each bar shows the open, high, low and close of the running delta inside the bar. This needs every print of the bar from the tape, so bars where the tape has gaps are left empty. Candles of CVD and Delta Volume draw a **body**, a **wick** and optionally a **border**, each switched in the Display tab; with a border, bodies are drawn lighter inside it. An unknown bar breaks the line, and the sum carries on after it. ### Per-venue CVD On an [aggregated market](/docs/app/aggregated-markets), the CVD pane also draws one line per venue. You can see at a glance whether Binance Futures is buying while Bybit sells. ## Trade Counter **Trade Counter** shows how many buy and sell trades printed in each bar. | Display | What it draws | |---|---| | Histogram | Buy and sell counts as bars | | Lines | Buy and sell counts as lines | | Net | Buys minus sells | Bars whose trade split is not known (bars built from candles) show the total count in grey as "side unknown". ## Net Positioning **Net Positioning** estimates net longs and net shorts opened per bar, with cumulative lines. It splits the change in open interest by taker side, with β = buy volume ÷ (buy + sell): | Open interest | Longs change by | Shorts change by | |---|---|---| | Rising | ΔOI × β | ΔOI × (1 − β) | | Falling | ΔOI × (1 − β) | ΔOI × β | ### Display | Display | Shows | |---|---| | Longs and shorts | Both bars side by side, with a cumulative line each (default) | | Net longs | Longs opened (bright) or closed (dim) per bar, with its cumulative line | | Net shorts | The same for shorts | | Delta | Net longs − net shorts per bar, with the cumulative net | **Style** draws the bars (Histogram) or **Cumulative candles**: each bar's candle opens where the previous one closed and closes at the cumulative sum, so trends in positioning read like price. With Longs and shorts, the candles follow the net (longs − shorts). | Setting | Effect | |---|---| | Cumulative lines | The running sums over the bars; reset daily, weekly or monthly with **Cumulative reset** | | Only new positions | Counts positions opened only; bars where positions closed show nothing | | Percent of open interest | Each value as a percentage of the open interest at the bar's close (the legend adds `% of OI`) | | Absorption flags | A triangle at the top of the pane over bars whose net positioning was at least twice the average of the 50 bars before and price closed against it: longs opened into a falling bar (pointing up), or shorts into a rising one (pointing down) | | Unit | Coin or USD at each bar's close | The legend calls it **NL/NS est.** because it is an estimate. For measured positioning on Hyperliquid, see [Hyperliquid positioning footprints](/docs/app/liquidations#hyperliquid-positioning-footprints). ## In Bar Stats [Bar Stats](/docs/app/bar-stats) can show delta, delta %, CVD, trade counts and the delta of each size band as rows under the bars. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/cvd-and-delta --- # Bar Stats > A table of order-flow statistics under the bars, one row per statistic and one cell per bar. **Bar Stats** puts a table under the chart. Each row is a statistic, each column is a bar, and each cell is shaded by magnitude. You read volume, delta, trade counts, size-band delta, liquidations, open interest, funding and depth for every bar at a glance, aligned with the bars above. Add it from the indicator dialog, group **Volume**. The default footprint chart already has it. ## Rows By default Bar Stats shows **Volume**, **Delta** and **Delta %**. Choose and reorder rows in the study's settings. ### Trade flow | Row | Short label | Meaning | |---|---|---| | Volume | Vol | Total traded volume | | Buy volume | Buy | Volume of buy aggressors | | Sell volume | Sell | Volume of sell aggressors | | Delta | Δ | Buy minus sell volume | | Delta % | Δ% | Delta as a share of volume | | Trades | Trades | Number of trades | | Buy trades | B trd | Number of buy trades | | Sell trades | S trd | Number of sell trades | | Average trade size | Avg | Volume per trade | | Largest trade | Max | Largest single print | | CVD | CVD | Running delta | | Lowest delta in the bar | Min Δ | Lowest running delta inside the bar (from the tape) | | Highest delta in the bar | Max Δ | Highest running delta inside the bar (from the tape) | ### Size bands One row per trade-size band: **Delta < $1K**, **Delta $1K–10K**, and so on up to **Delta ≥ $5M**. Their short labels are `Δ < $1K` and so on. See [trade-size bands](/docs/app/cvd-and-delta#trade-size-bands). ### Derivatives and book | Row | Short label | Meaning | |---|---|---| | Sell liquidations (longs) | Liq L | Longs liquidated in the bar (forced sells) | | Buy liquidations (shorts) | Liq S | Shorts liquidated (forced buys) | | Delta liquidations (buy − sell) | Liq Δ | Shorts liquidated minus longs, as Market Monkey's | | Total liquidations | Liq | Both together | | Buy / Sell liquidation trades | Liq B trd, Liq S trd | Liquidation prints in the bar, counted from the prints the market holds (empty before the first held) | | CVD $1–1K … CVD $5M+ | CVD $1K–10K … | Market Monkey's CVD per trade-size bucket: each bucket's running delta (its $1M–5M one row) | | Open interest | OI | Open interest at the bar's close | | Open interest Δ | OI Δ | Change of open interest over the bar | | Funding % | Fund % | Funding rate at the bar's close | | Bid depth ±1 % | Bid 1% | Resting bids within 1 % of the mid | | Ask depth ±1 % | Ask 1% | Resting asks within 1 % of the mid | | Depth imbalance ±1 % | OB 1% | (bids − asks) ÷ (bids + asks), in percent | | Net longs (est.) | NL | Estimated longs opened, see [Net Positioning](/docs/app/cvd-and-delta#net-positioning) | | Net shorts (est.) | NS | Estimated shorts opened | | Depth imbalance ±0.1 % … ±10 % | OB 0.1% … | The imbalance within one band: ±0.1, 0.25, 0.5, 2, 2.5, 5 or 10 % of the mid | ## Markets **Markets** in the settings lets Bar Stats read another market of the chart's coin, or several summed, instead of the chart's: for example Bybit's statistics under a Binance chart. Its rows come from that market's bars at the chart's interval, matched to the chart's bars by open time; a chart bar without one shows empty cells. The legend names the choice (`Stats · Bybit`). See [Markets of an indicator](/docs/app/heatmap#markets). ## Unit **Value units** (Coin, USD, or **Follow global**: the app's unit, Market Monkey's "global", the default) shows every row that counts the coin (volumes, delta, trade sizes, CVD, size bands, open interest, depth, net longs and shorts) in USD at each bar's close, and the liquidation rows (notional) in the coin at each bar's close unless USD; counts, percentages and funding stay as they are. Market Monkey's Mark Price stat has no counterpart: the mark price is not recorded. New Bar Stats start as Market Monkey's: grading over the whole chart, no row labels (inside the plot when on), cells meeting without gaps (the cell grid draws lines between them). ## Colour grading Cells are shaded by magnitude. **Colour grading** sets what a cell is compared with: | Grading | Compared with | |---|---| | Bar | The other cells of the same bar | | Day | Cells of the same row within the day | | Week | Cells of the same row within the week | | Visible range | Cells of the same row on screen (default) | | Whole chart | Cells of the same row in every bar loaded | Signed rows like delta use the bull colour above zero and the bear colour below. Buy rows are always bull, sell rows always bear, and the rest are grey by magnitude. Each row can take its own colours instead: the two swatches beside it in the row list (above zero, below zero). **Colour from (% of the largest)** leaves cells uncoloured below that share of the largest value they are compared with; their numbers stay. At 0 (default) every cell is shaded. **Cell grid** outlines every cell, **Grid thickness** sets its width and the swatch under the rows its colour. **Row labels** on the right of the pane can be turned off. **Layout** puts them on the price scale under the legend's strip (default), or, as Market Monkey does, inside the plot at its right with the rows filling the whole pane. Text appears in a cell when bars are at least about 26 px wide. Zoom in to read numbers; zoomed out, the shading alone shows the pattern. ## Where the numbers come from | Rows | Source | |---|---| | Volume, delta, trade counts, size bands | The engine's per-bar statistics, also from recorded history | | Largest trade, lowest and highest delta inside a bar | Measured from the raw tape, only for bars whose prints add up to their volume | | Liquidations, open interest, funding, depth | Per-minute context, copied onto each bar at its close | | Net longs and shorts | The estimate from open interest and taker side | Book depth is sampled once a second. Open interest and funding before the session are what the data hub recorded: open interest in 5-minute or hourly points, funding as the rate recorded just before each settlement. A value that is not known is shown as missing, never as zero. Spot markets have no liquidations, open interest or funding. ## Related panes Some of these statistics also exist as their own panes in the indicator dialog: | Study | What it draws | |---|---| | Open Interest | Open interest as a line | | Funding Rate | Funding at each bar's close, in percent | | OB Depth | Bid and ask depth within ±0.1 to ±10 % over time, as lines or stacked areas, with an imbalance strip | | Trade Counter | See [CVD and delta](/docs/app/cvd-and-delta#trade-counter) | | Net Positioning | See [CVD and delta](/docs/app/cvd-and-delta#net-positioning) | Source: https://flowscope.91-99-118-129.sslip.io/docs/app/bar-stats --- # Liquidations > Liquidation markers, modelled liquidation levels and measured Hyperliquid take-profit, stop and liquidation maps. Flowscope shows liquidations three ways: the forced liquidations that already happened, where liquidations are likely to sit according to a model, and, on Hyperliquid, where real positions have their liquidations, stops and take-profits. | Study | Group | Source | Measured or modelled | |---|---|---|---| | Liquidations | Positioning | Venue liquidation streams | Measured | | Liquidation Heatmap | Heatmaps | Open interest and taker flow | Modelled | | Hyperliquid Take Profit Heatmap | Heatmaps | Hyperliquid accounts | Measured | | Hyperliquid Stop Loss Heatmap | Heatmaps | Hyperliquid accounts | Measured | | Hyperliquid Liquidations Heatmap | Heatmaps | Hyperliquid accounts | Measured | | Footprint Hyperliquid OI, NL, NS | Footprint | Hyperliquid trades and accounts | Measured | ## Liquidation markers The **Liquidations** study marks forced liquidations on the price pane, longs and shorts in their own colours. Set **Hide below USD** to show only the large ones. It is on the heatmap charts of the presets by default. Liquidations come from the venues' websocket streams on derivatives markets. Per-bar totals are also available as [Bar Stats](/docs/app/bar-stats) rows. ## Liquidation heatmap {#liquidation-heatmap} The **Liquidation Heatmap** estimates where open positions would be liquidated, on Binance Futures, Bybit and OKX. Aggregated markets sum the venues. How the model works: 1. Added open interest in each interval is attributed to longs or shorts by taker side, at the interval's VWAP. 2. Each entry is projected to liquidation prices at 5×, 10×, 25×, 50×, 75× and 100× leverage, weighted 10 %, 25 %, 30 %, 15 %, 8 % and 12 %, with a maintenance margin of 0.4 %. 3. A level is removed when price trades through it. Falling open interest shrinks the closing side. Levels are recorded as runs and drawn like the order-book heatmap. Its settings follow Market Monkey's tabs: **General** (show heatmap, show profile, profile outline, extend latest, quick toolbar, ticks per row), **Style** (colormap, opacity, gamma, profile width and scale), **Leverage Bands**, then Flowscope's **Advanced**. A new one starts as Market Monkey's: the Dark colormap solid from its first colour, intensity from zero, the profile with its outline. ### Profile at the price axis **Profile of the standing levels at the price axis** (on by default) draws the levels standing now as a horizontal profile at the right edge of the chart, left of the book profile, as Market Monkey's: each row's bar in the heatmap's colours (its size on the heatmap's scale) and, with **Profile outline**, a curve of the running sum from the price outwards, in the chart's up colour above the price (shorts) and its down colour below (longs). | Setting | Effect | |---|---| | Profile | **Filled** rows (with the outline when it is on), or the **Outline** alone | | Profile width (px) | 90 by default | | Profile scale | Stretches the bars (the largest may run past the width), 1.0 by default | | Ticks per row | The row height in ticks, of the profile and of the heatmap: with a value the heatmap draws cells of a bar by a row (the levels in it summed), as Market Monkey's; 0 (default) keeps the sharp lines and picks profile rows about 4 px tall | | 5× … 100× leverage | Which leverage tiers the profile and the heatmap show | Every standing level keeps its notional per leverage tier, also on aggregated markets, so turning tiers off shows, for example, only the high-leverage levels close to price. Levels carried over from recorded history count by the tiers' weights until positions open at their price again. The heatmap and its history keep only totals, so there a level counts toward the tier whose liquidation distance (1/leverage less the maintenance margin) is nearest its distance from each bar's close: with 25× to 100× on it shows the levels within about 6 % of the price at the time, with 5× and 10× only those farther away. > [!IMPORTANT] > This is a model. Real leverage and entry prices are not public on these venues. Treat the bands as zones where liquidations are plausible, not as orders. ## Hyperliquid maps Hyperliquid is transparent: every trade names its buyer and seller, and any account's positions and orders can be read. Flowscope uses that to show where real positions keep their take-profits, stops and liquidation prices. These maps are measured, not modelled. The three heatmaps: | Heatmap | Draws | |---|---| | Take Profit | Resting take-profit orders: long TPs (sells above) and short TPs (buys below) | | Stop Loss | Resting stop-loss orders of longs and shorts | | Liquidations | Liquidation prices of longs and shorts, sized `\|size\| × liquidation price` | Every 10 seconds each coin is sampled into price buckets of about 0.02 % of the price. Position TP/SL orders count the whole position; sized ones count at most the position. Triggers that cannot close the position, and levels more than 4× away from the mark, are ignored. Each heatmap has its own colour ramp (Thermal by default), range slider, contrast, saturation and opacity. ### Which charts show them The maps are per Hyperliquid coin, and Flowscope finds the coin from any market of it: `BTCUSDT`, `BTC-USDT-SWAP` and the aggregate `BTC` all map to Hyperliquid `BTC`; `1000PEPE` maps to `kPEPE`. You can lay the Hyperliquid maps over a Binance chart of the same coin. A market with no source shows a note instead. ### Coverage The maps are only as complete as the set of accounts Flowscope has read. **Coverage** is the share of Hyperliquid's open interest held by the tracked accounts. | Mode | How accounts are found | Typical coverage | |---|---|---| | From the hub | The complete trade stream plus a leaderboard seed of accounts worth $50k or more | 32–41 % of open interest after 15 minutes (measured on BTC, ETH, SOL, HYPE) | | In-process (a coin the hub does not record) | Samples of the latest 10 trades every 5 seconds per open coin | Lower: BTC reached 3.8 % of open interest after 5 minutes in a test with in-process budgets | For a coin Capture collects, Flowscope loads the last 3 hours from the hub, refreshes every 10 seconds and loads older history as you scroll back; other coins have no maps. See [Data: Hyperliquid](/docs/data/hyperliquid). ## Hyperliquid positioning footprints Three footprint studies draw, per bar and price, what Hyperliquid trades did to positions: | Study | Each cell shows | |---|---| | Footprint Hyperliquid OI | Change of open interest: buyer's plus seller's change of long size | | Footprint Hyperliquid NL | Net longs: longs opened minus longs closed by the aggressor | | Footprint Hyperliquid NS | Net shorts: shorts opened minus shorts closed by the aggressor | A taker buy that flips a short into a long counts as a closed short plus an opened long. **Display** draws the values as a **cluster** (numbers in shaded cells) or a **profile** (bars growing from the bar's left edge). These footprints need the complete trade stream, which [Capture follows](/docs/data/hyperliquid). Each minute also records how much of the volume each figure covers; open-interest changes are counted only where both sides of a trade are tracked. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/liquidations --- # Gamma exposure > Where BTC and ETH options make dealers hedge, from Deribit's option chains: call wall, put wall, gamma flip and the exposure per strike. The **Gamma Exposure** study (GEX) estimates where options make dealers trade the underlying as the price moves. It reads every listed BTC or ETH option on Deribit, where most crypto options trade, and draws three levels across the chart (the call wall, the put wall and the gamma flip) and the exposure of each strike as a profile at the right edge. ## What it shows A dealer who sold or bought an option hedges it in the underlying. How much that hedge has to change for a given move is the option's gamma. Gamma is largest at strikes near the price and grows as expiry approaches, so where a lot of open interest sits close to the price, hedging becomes a large share of the flow. Who holds which options is not published, so the study has to estimate dealers' positions. It offers two ways (setting **Dealer positions**): - **From the options flow** (default). Dealers make the market, so they hold the other side of what takers trade. The hub records every Deribit option trade with the side that took it, and adds up each option's net taker buying. Where takers bought, dealers count as short; where takers sold, as long. That covers only the part of the open interest that recorded trades account for. The rest, opened before recording began, falls back to the usual reading. - **Usual (long calls, short puts)**. The usual reading: traders buy puts and sell calls, so dealers are long the gamma of calls and short the gamma of puts. Call exposure counts positive, put exposure negative. Positive exposure means dealers are long gamma there, negative short. With positions from the flow, a call strike can be negative and a put strike positive. | Element | What it is | |---|---| | Profile | Net exposure per strike (calls plus puts), as bars at the right edge of the chart: positive strikes in one colour, negative in another. Bar length is relative to the largest strike in view. | | Call wall | The strike with the most call exposure | | Put wall | The strike with the most put exposure | | Gamma flip | The price where the chain's net exposure changes sign, nearest the current price. Drawn dashed. | | Legend | The expiries counted and the net exposure of all of them, for example `+$168M / 1 %`. With positions from the flow, also how much of the open interest counted the recorded flow accounts for, for example `flow 12 % of OI` | ## How to read it | Where | Dealers | What price tends to do | |---|---|---| | Positive gamma (long) | Sell into rallies and buy dips | Stall, mean-revert, pin near large strikes | | Negative gamma (short) | Sell into falls and buy into rallies, with the move | Accelerate, extend moves | - The **call wall** often acts as resistance and the **put wall** as support: hedging flow is largest there. - With calls above the price and puts below, as is usual, the net exposure is positive above the **gamma flip** and negative below it: moves tend to be damped above it and to run below it. A price crossing the flip is where that behaviour is expected to change. - A positive total in the legend says the chain as a whole damps moves; a negative total says it amplifies them. - Short-dated options carry the most gamma near the price. Use **0DTE** or **Next expiry** to see what matters today, **All** for the structure of the whole chain. > [!IMPORTANT] > This is a model. Positions are not public, so the sign of each option is an assumption, and real dealer books differ from it. Crypto option open interest is also small next to perpetual futures volume, much smaller than the share index options have in equities, so hedging flow is one force among many. Treat the levels as zones, not as prices that will hold. ## Adding it to a chart Add **Gamma Exposure** from the indicator dialog (/), group **Positioning**; it is also in the **Official** list. It draws over the price pane. The study works on any market of BTC or ETH: Binance `BTCUSDT`, OKX `BTC-USDT-SWAP`, Hyperliquid `BTC` and the aggregate `BTC` all read Deribit's BTC options. Strikes are in US dollars, so on a market quoted in USDT or trading at a basis to Deribit's index the levels can sit a little off that market's price. When the [Order Book Profile](/docs/app/heatmap#order-book-profile) is on the same chart, the gamma profile sits to its left. The profile needs a plot wider than 240 px. ## Settings ### Inputs | Setting | What it does | Default | |---|---|---| | Expiries | Which options count: All, 0DTE, Next expiry, This week or This month | All | | Dealer positions | How dealers' positions are estimated: from the options flow, or the usual reading (long calls, short puts); see [What it shows](#what-it-shows) | From the options flow | | Call wall, put wall and gamma flip | Draw the three levels across the chart | On | | Level labels | Boxed name and price of each level at the right edge | On | | Profile at the right edge | Draw the exposure per strike | On | | Profile width (px) | Width of the longest bar, 40 to 400; at most a quarter of the plot | 260 | | Exposure values beside the bars | Signed dollar exposure per 1% move, next to each visible profile bar | On | | Include levels in automatic price scale | Fit the three visible key levels together with candles; manual price scaling remains available | On | | Historical wall and flip lines | Plot recorded wall/flip values over time; missing history remains a gap | On | | Exposure over time behind the bars | Draw recorded chains as background bands | Off | | Opacity over time | How strong the bands over time are drawn | 0.5 | The expiry filters: | Expiries | Counts options that | |---|---| | All | Have not expired | | 0DTE | Expire within the next 24 hours, whenever they were listed: the daily option, and a weekly, monthly or quarterly one on its last day | | Next expiry | Expire at the nearest expiry | | This week | Expire within 7 days | | This month | Expire within 31 days | Deribit options expire at 08:00 UTC. The windows are counted from now, not by calendar week or month. ### Style | Plot | Draws | Default colour | |---|---|---| | Positive gamma | Profile bars of strikes with positive net exposure | Green | | Negative gamma | Profile bars of strikes with negative net exposure | Rose | | Call wall | The call wall line | Green, 1 px | | Put wall | The put wall line | Rose, 1 px | | Gamma flip | The gamma flip line, dashed | Yellow, 1 px | Hiding a level's plot on the Style tab hides that level alone. ## How it is computed For every option the study takes its strike, open interest (in the coin), mark implied volatility and the price of the future it settles on. For each option: 1. Gamma is Black-Scholes gamma on the option's own underlying (its expiry's future) with its mark volatility and the time left to expiry. Time left is at least five minutes, so an option about to expire does not blow up. 2. The dealer position is estimated, in the coin, long positive: - **Usual**: the open interest, positive for calls and negative for puts. - **From the options flow**: the recorded net taker buying `flow`, held to at most the open interest, counts against the takers: dealers hold `−flow`. The rest of the open interest, `open interest − |flow|`, counts by the usual reading. 3. Exposure is dollars per 1 % move of the price: `gamma × dealer position × price² × 1 %`. 4. Exposures are summed per strike. Strikes without open interest are left out. The **call wall** and **put wall** are the strikes with the largest call and put exposure, not the largest open interest: a strike near the price can outweigh a larger one far away. The **gamma flip** is found by moving the price from 80 % to 120 % of the current index in steps of 0.25 %, moving every option's underlying with it, and summing the chain's exposure at each step. Where the sum changes sign, the crossing is interpolated; the one nearest the current price is the flip. If the sum keeps one sign over that whole range there is no flip, and none is drawn. The exposure is computed on an engine job, never on the interface thread, again when a new chain arrives, when the expiry filter or the dealer positions change, and each minute as time to expiry shrinks. The flow is net taker buying, not positions: a trade that closes a position counts the same as one that opens it. Over time, though, closing trades cancel the opening trades they undo, as long as both were recorded. ## Where the data comes from {#data} Capture asks Deribit for the whole option chain of each configured coin once a minute, in one request, and records it: every option's expiry, strike, call or put, open interest, mark volatility, underlying price and recorded flow, and the coin's index price. Just before each chain it asks for the option trades since the last ones it saw and adds each trade's amount to its option's flow: plus where the taker bought, minus where the taker sold. The flow starts when capturing starts; nothing before it is fetched. After a restart Capture goes on from its checkpoint and asks for the trades missed meanwhile. The app asks the hub for the newest chain once a minute while a chart shows the study; it never asks Deribit itself. This is the same rule as for every other kind of history: Flowscope reads what the archive holds (see [History and data](/docs/app/history-and-data#hub)). Capture collects BTC and ETH by default (see [Files](/docs/data/storage#option-chain-files)). The chains are served at [`/v1/options/`](/docs/api/options). ## Limits - **BTC and ETH only.** Those are the coins with Deribit options. Other coins show the note "options are listed for BTC and ETH (Deribit), on a data hub". - **Deribit only.** Options listed on other venues are not counted. - **A data hub is needed.** Without one, or with option recording turned off on the hub, the study shows a note instead of levels. Until the first chain arrives it shows "loading the option chain…"; if the hub cannot answer, the note gives the reason. - **Levels now, bands over time.** The walls, the flip and the profile are those of the newest chain, drawn across the whole chart. The past shows as the bands behind the bars, from the chains the hub recorded. - **Once a minute.** The chain the study reads is at most about two minutes old: the hub takes one a minute and the app asks for the newest once a minute. - **Flow since recording began.** The flow covers trades from when the hub began recording on. Open interest from before then counts by the usual reading, so right after the hub starts both settings show nearly the same picture; the legend's `flow … % of OI` shows how much the flow covers. - **A model.** The dealer positions, the Black-Scholes gamma and Deribit's mark volatility are all approximations. See the note in [How to read it](#how-to-read-it). ## Related - [Liquidations](/docs/app/liquidations): liquidation markers and modelled liquidation levels, another view of where forced flow sits. - [Liquidity heatmap](/docs/app/heatmap): resting limit orders, to compare with the walls. - [Indicators](/docs/app/indicators): the full study catalog. Gamma calculations run in a separate WASM worker, so historical option roots do not block the chart UI or the heatmap worker. The hub reads the chains Capture collected; it does not collect a second chain. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/gamma-exposure --- # Aggregated markets > One coin across venues, with a summed book and heatmap, a merged tape and per-venue CVD. An aggregated market is one coin summed across venues. Instead of watching BTC on five charts, you watch one: the combined book, the combined tape and the combined footprint, with CVD broken down by venue. ## Open one In symbol search, pick the market whose venue is **All venues**. Its symbol is the coin alone, for example `BTC`. The popular ones are BTC, ETH, SOL, XRP and DOGE. The **Orderflow desk** preset opens a footprint of BTC on all venues, and **BTC on every venue** puts the aggregate next to each venue's own chart. ## Components By default, for a coin `C`, the aggregate combines: | Venue | Market | |---|---| | Binance Futures | `CUSDT` (primary) | | Bybit | `CUSDT` | | OKX | `C-USDT-SWAP` | | Hyperliquid | `C` | | Deribit | `C-PERPETUAL` for BTC and ETH (inverse, the whole book), `C_USDC-PERPETUAL` otherwise | | Binance | `CUSDT` spot | | Coinbase | `C-USD` spot, the whole book (where Coinbase lists the coin) | | Bybit Spot | `CUSDT` spot | | OKX Spot | `C-USDT` spot | | Kraken | `C/USD` spot | Binance COIN-M (`CUSD_PERP`), Bybit Inverse (`CUSD`) and Deribit Spot (`C_USDT`, BTC and ETH) are not in the default set; tick them in the **Markets** picker to add them. The picker lists the venues in three columns, each with **All**: linear perpetuals, coin-margined (inverse) perpetuals and spot. Opening the aggregate opens its component markets. To choose them, open the chart's or pane's settings: the **Markets** picker lists every venue in a spot and a perpetuals column, and what you tick is summed. A component already open on another chart is shared, not opened twice. ## What is combined, and how | Data | How the aggregate builds it | |---|---| | Order book and heatmap | Each component forwards its book and every accepted change. The aggregate keeps each component's contribution per exact price and applies only the differences. The summed book feeds the aggregate's own heatmap. | | Tape | The union of the component tapes, merged in time order. When a component loads older history, the tapes are merged again. | | Bars, footprints, CVD, bubbles | Built from the merged tape and summed minutes, by the same code as any market | | Open interest | Sum of the components' latest values, once every component has reported | | Funding | Weighted by open interest, normalized to 8 hours | | Instrument and candles | From the primary component, Binance Futures | | Modelled liquidation levels | Sum of the venues that have them | Because each component's book contribution is tracked separately, a venue that resynchronizes or disconnects removes exactly what it added. The rest of the book is untouched. The tape really is the union. A live check counted 9 721 aggregate prints against 4 055 + 4 028 + 1 028 + 559 + 51 from the five venues. ## Per-venue CVD The CVD pane of an aggregate draws one line per venue next to the total. Turn this off with **Per-venue lines** in the CVD settings. See [CVD and delta](/docs/app/cvd-and-delta#per-venue-cvd). ## History Aggregate footprint history is what was recorded for its venues: a minute is **Partial** where one of them was not recorded. Capturing every venue of the aggregate makes it whole ([Data](/docs/data)). Footprint requests on an aggregate are forwarded to the components, and the aggregate sums their minutes again whenever one changes. ## Limits today - Quantities are summed as each venue reports them in base units. > [!NOTE] > A per-venue footprint breakdown and inverse-contract normalization are on the roadmap. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/aggregated-markets --- # Trading and paper accounts > Trade from the order ticket, the chart and the ladder; paper accounts fill against the live market with real queues, fees and funding. Flowscope trades from where you read the market: an order ticket on the right, orders and positions on every chart, click trading on the ladder and an account panel under the panes. Every account starts as a **paper account**: simulated fills against the live market, so you can practise and test ideas without funds. [Live accounts](/docs/app/live-trading) use the same ticket, lines and panel. ## Open the trading tools - **Trade** in the top bar (Alt+O) opens the order ticket on the right and the account panel under the panes. - **Trading panel** in the bottom bar (Alt+P) shows or hides the account panel alone. The ticket trades the market of the active pane. An aggregated chart ("All venues") trades on its primary venue, Binance futures; with a live account in use, the same coin on that account's venue. ## How paper accounts fill Paper fills follow the real market, not the last price alone: | Order | Fills | |---|---| | Market | Sweeps the shown order book from the best price (slippage), taker fee | | Limit that crosses | Takes what the book holds up to its price, the rest rests | | Resting limit | Joins the back of the queue: the size already resting at its price must trade first. A print through its price fills it whole | | Stop, stop limit, take profit | Fire when a print reaches the trigger, then work as market or limit | | Trailing stop | The trigger follows the best price by the trailing distance | Fees are each venue's base tier (or your own, or none, in the account's settings). Funding is paid or received at every funding period. Margin is cross or isolated per market with leverage up to the venue's maximum; an isolated position is liquidated when its margin runs out, a cross account when its equity falls to the maintenance margin of its positions. ## The order ticket | Part | What it does | |---|---| | Account chip | The account in use (amber dot: paper, green: live), its equity; the menu switches accounts, creates paper accounts, connects venues | | Market, Limit, Stop, Algo | Order types; Stop offers stop market, stop limit and trailing | | B / M / A | Fill the price field with the best bid, mid or best ask | | Size | In the coin or in USD; the slider sets a share of what is available at your leverage | | `10x · Cross` | Leverage and margin mode for the market | | Reduce only, Post only, TIF | Order flags: post only is rejected if it would take; GTC, IOC, FOK | | Take profit / Stop loss | Prices that become the position's TP and SL once the entry fills | | Buy / Long, Sell / Short | Place the order | | Position card | Size, entry, unrealized P&L, liquidation price; close, close half, reverse | ## Algos The ticket's **Algo** tab holds three execution helpers: - **Scale**: a ladder of 2 to 50 limit orders between two prices, with a size skew from equal sizes to nine times more at one end. - **TWAP**: a size spread over time in 2 to 200 lots, with jitter, irregular lot sizes, chased slices (each lot rests at the touch and follows it) and price bounds (pause or skip a lot while the price is outside). - **Chase**: a limit that follows the best bid or ask (or one tick inside the spread) until it fills, giving up after a move you choose. Running algos show at the top of the ticket and in the panel's **Algos** tab with their progress; pause, resume or stop them there. ## On the chart - The **position** is a solid line at its entry with its size and P&L. Drag **TP** or **SL** out of its label to set a take profit or stop loss; **×** closes the position at market. - **Orders** are dashed lines in their side's colour. Drag a line or its label to move the order; **×** cancels it. - The **liquidation price** is a dotted amber line; **fills** are small arrows on their bars. - Right-click the chart for **Buy / Sell limit or stop at** the price under the pointer, sized by the ticket. ## On the ladder Arm the ladder with its bolt in the toolbar. A strip under the toolbar holds the order size in USD (with $100 to $1K presets), the position with reverse and close, and cancel. Click left of the price to buy and right of it to sell: below the market that is a limit, above it a stop (and the other way round for sells). Working orders show as tags on their rows; drag a tag to another row to move the order, click its × to cancel. ## The account panel The row at the top shows balance, equity, realized and unrealized P&L, position and order margin, what is available and the margin buffer. The tabs: | Tab | Shows | |---|---| | Positions | Every position with entry, mark, liquidation price, margin, P&L and TP/SL; edit TP/SL, reverse, close | | Orders | Working orders; move or cancel them | | Algos | Running and finished algos | | Order history | Every order that ended: filled, cancelled or rejected, with the reason | | Trade history | Round trips from opening a position to closing it, with fees, funding, net P&L and return | | Balance history | Every change of the balance: realized P&L, commissions, funding | | Activity log | What was sent and what came back | | Analytics | Win rate, profit factor, expectancy, average win to loss, drawdown and the cumulative P&L curve | Click a symbol to show its market on the active pane. The copy icon puts a history tab on the clipboard as CSV. ## Paper accounts Create as many as you like from the account menu (**New paper account…**), each with its own starting balance. **Account settings…** changes the name, fees, slippage and the leverage and margin mode new positions start with, resets an account to a balance, or deletes it. Signed in, paper accounts sync with your Flowscope account like layouts and preferences. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/trading --- # Live trading > Connect Hyperliquid, Binance, Bybit, OKX or Aster and trade them from the same ticket, chart and ladder. Keys stay on your device. A live account is a venue account connected to Flowscope. It uses the same order ticket, chart lines, ladder and account panel as a [paper account](/docs/app/trading); positions, orders, fills and history come from the venue. ## Keys stay on your device Flowscope signs every order on your computer (or in your browser tab) and sends it to the venue directly. Keys are never sent to Flowscope's servers and never synced with your account. Create keys that **cannot withdraw**: an API wallet on Hyperliquid, trade-only API keys elsewhere. Connected accounts are remembered on the device when you leave **Remember on this device** on; **Account settings… → Disconnect** removes the account and its key. ## Connect a venue Open the account menu (the account chip in the ticket or the panel) and choose **Connect a venue…**. | Venue | What you enter | Where to create it | |---|---|---| | Hyperliquid | Your account address and an API wallet's private key | app.hyperliquid.xyz → More → API; approve the API wallet with your wallet | | Binance | API key and secret with futures trading | API Management; USDⓈ-M futures in one-way mode | | Bybit | API key and secret (contracts, read-write) | API Management; USDT perpetuals on the unified account | | OKX | API key, secret and passphrase (trade) | My API; USDT swaps. Demo keys work on the Demo network | | Aster | API key and secret | API Management on asterdex.com | A newly connected account becomes the account in use. The account chip shows a green dot and **LIVE** while it is connected; if the venue refuses the key the chip turns red and says why. ## Every order asks first Orders, closes and reversals for a live account open a confirmation that says exactly what will be sent ("Buy 0.0116 BTC at market on Hyperliquid…"). Tick **Don't ask again on this device** to skip it from then on. ## What each venue does | | Hyperliquid | Binance | Bybit | OKX | Aster | |---|---|---|---|---|---| | Market orders | IOC limit 3 % through the touch | ✓ | ✓ | ✓ | ✓ | | Stops and take profits | Trigger orders | Algo orders | Conditional orders | Algo orders | ✓ | | Position TP/SL | ✓ | Close-position orders | On the position | Conditional | Close-position orders | | Trailing stop | Paper only | ✓ | Paper only | ✓ | ✓ | | Updates | Live (websocket) | Every few seconds | Every few seconds | Every few seconds | Every few seconds | With a live account in use, every pane trades the same coin on that account's venue: a Binance BTCUSDT chart places Hyperliquid BTC orders while a Hyperliquid account is active, and the ticket says so. ## Algos on live accounts Scale orders, TWAP and chase run on live accounts too. TWAP lots go out as market orders; a chase places its limit and moves it at most once a second. Algos on live accounts run while the app is open and never resume on another device. Chased TWAP slices are paper-only for now. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/live-trading --- # Indicators > The indicator dialog, the full study catalog and study settings. Everything a chart draws besides its main series is a study: order-flow layers like the heatmap and CVD, profiles, and classic indicators like RSI. Each study has inputs, plots with their own colours, and a place: over the price (overlay) or in its own pane below. ## Add a study Press /, click **Indicators** in the top bar, or choose **Add indicator…** from the chart's context menu. The dialog is called **Indicators, metrics and strategies**. | Section | What it lists | |---|---| | Search | Type to filter by name | | Favourites | Studies you starred. Liquidity Heatmap, CVD, Volume Profile and VWAP are starred by default. | | Templates | Your saved [indicator templates](/docs/app/layouts#indicator-templates) | | Official | The curated list, including the Candles and Footprint chart types | | All | Every study | | Groups | Heatmaps, Footprint, Volume, Delta / CVD, Positioning, Profiles, Technicals | Filter the list to **All**, **Overlays** or **Panes**. Click a row to add the study to the active chart; the dialog stays open so you can add several, and the row confirms with **Added** (or **Added ×2** for a second copy). Star a row to make it a favourite. Studies that need live order-flow data carry a badge. ## Catalog ### Heatmaps | Study | Place | What it shows | |---|---|---| | Liquidity Heatmap | Overlay | Resting limit orders over time, see [Liquidity heatmap](/docs/app/heatmap) | | Liquidation Heatmap | Overlay | Modelled liquidation levels, see [Liquidations](/docs/app/liquidations#liquidation-heatmap) | | Hyperliquid Take Profit Heatmap | Overlay | Resting take-profits of Hyperliquid positions | | Hyperliquid Stop Loss Heatmap | Overlay | Resting stops of Hyperliquid positions | | Hyperliquid Liquidations Heatmap | Overlay | Liquidation prices of Hyperliquid positions | ### Footprint | Study | Place | What it shows | |---|---|---| | Footprint Hyperliquid OI | Overlay | Change of Hyperliquid open interest per bar and price | | Footprint Hyperliquid NL | Overlay | Hyperliquid net longs per bar and price | | Footprint Hyperliquid NS | Overlay | Hyperliquid net shorts per bar and price | The footprint itself is a chart type, see [Footprint](/docs/app/footprint). ### Volume | Study | Place | What it shows | |---|---|---| | Volume | Overlay | Bar volume split by aggressor, coloured by candle, or as a Delta highlight (the total dark, \|delta\| bright over it, in the colour of the side that dominated) at the bottom of the chart, in coin or USD, with an optional moving average | | Trade Bubbles | Overlay | The largest market orders as circles | | Bar Stats | Pane | A table of order-flow statistics, see [Bar Stats](/docs/app/bar-stats) | | Trade Pulse | Pane | The latest prints drifting left over 15 s to 5 min, sized by notional, buys above and sells below | | Trade Counter | Pane | Buy and sell trade counts per bar as histograms, lines or net, with an average | ### Delta / CVD | Study | Place | What it shows | |---|---|---| | Delta Volume | Pane | Buy minus sell volume per bar, by trade size, in coin, USD or percent of the bar's volume, with an average | | Cumulative Volume Delta | Pane | Running delta as a line or candles, or each bar's delta as a histogram, by trade size and venue, in coin or USD, restarting never, daily, weekly or monthly, with an average | See [CVD and delta](/docs/app/cvd-and-delta). ### Positioning | Study | Place | What it shows | |---|---|---| | Open Interest | Pane | Outstanding contracts as a line, an area, its change per bar or candles (O H L C per bar), in coin or USD, with an average; history is what the data hub recorded | | Funding Rate | Pane | Funding at each bar's close, per period or annualized, positive and negative in their own colours, with an average | | Net Positioning | Pane | Estimated net longs and shorts per bar, with cumulative lines that can restart daily, weekly or monthly | | Liquidations | Overlay | Forced liquidations as markers sized by notional; longs, shorts or both, the largest labelled | | Liquidation Volume | Pane | Liquidated shorts (B) and longs (S) per bar, mirrored, stacked or net, in USD or coin, with an average | | OB Depth | Pane | Bid and ask depth within ±0.1, 0.25, 0.5, 1, 2, 2.5, 5 or 10 % (default 5 %) over time as lines, stacked areas or a net histogram, with an imbalance strip | | Order Book Profile | Overlay | The current book as bars at the right edge, near the price or all visible, per row or cumulative, with the largest sizes | | Gamma Exposure | Overlay | Where BTC and ETH options on Deribit make dealers hedge: call wall, put wall, gamma flip and the exposure per strike at the right edge, for all expiries or only 0DTE, the next, this week's or this month's; see [Gamma exposure](/docs/app/gamma-exposure) | ### Profiles | Study | Place | What it shows | |---|---|---| | Volume Profile (Visible Range) | Overlay | Volume at price for the visible bars | | Session Volume Profile (VPSV) | Overlay | A volume profile per session with developing levels | | TPO Profile (Market Profile) | Overlay | Letters per period at price per session | | Naked POCs | Overlay | Points of control price has not traded through since, optionally only near the price, labelled nPOC or with their price | | VWAP | Overlay | Volume-weighted average price, anchored per session, week or never, with up to three deviation bands | | Cash Sessions | Overlay | The regular hours of the Tokyo (09:00–15:30), London (08:00–16:30) and New York (09:30–16:00) stock markets in their local time, daylight saving included, and an optional custom session in UTC: boxes from each session's high to its low or background bands, the session's name and range in percent, its open and midpoint, and its high and low carried on until the next session of the same market. The legend says which sessions are open and when they close, or which opens next | See [Profiles](/docs/app/profiles). ### Technicals | Study | Place | Default inputs | |---|---|---| | Moving Average Simple (SMA) | Overlay | Length 20, source | | Moving Average Exponential (EMA) | Overlay | Length 21, source | | Moving Average Weighted (WMA) | Overlay | Length 20, source | | Bollinger Bands | Overlay | Length 20, StdDev 2, source | | Keltner Channels | Overlay | Length 20, multiplier 2, ATR length 10 | | Donchian Channels | Overlay | Length 20 | | SuperTrend | Overlay | ATR length 10, factor 3, offset | | Ichimoku Cloud | Overlay | Conversion 9, base 26, leading span B 52, displacement 26 | | Relative Strength Index | Pane | Length 14, source; levels 70, 50, 30 | | MACD | Pane | Fast 12, slow 26, signal 9, source | | Stochastic | Pane | %K 14, smoothing 1, %D 3; levels 80, 20 | | Stochastic RSI | Pane | RSI 14, stochastic 14, %K 3, %D 3; levels 80, 20 | | Commodity Channel Index | Pane | Length 20; levels 100, 0, −100 | | Williams %R | Pane | Length 14, smoothing line; levels −20, −80 | | Rate Of Change | Pane | Length 9, source, smoothing line | | Money Flow Index | Pane | Length 14, smoothing line; levels 80, 20 | | Average True Range | Pane | Length 14 | | On Balance Volume | Pane | — | | Chaikin Money Flow | Pane | Length 20, smoothing line | ## Study settings Open a study's settings from its legend row (gear, or double-click the row) or from the object tree. The settings come in tabs. A simple indicator has **Inputs**; order-flow layers group theirs: | Layer | Tabs | |---|---| | Heatmaps | General, Interaction | | Liquidation heatmap | General, Interaction, Profile, Leverage bands | | Volume profiles | Display, Row / Scaling, POC, Value Area (and Session) | | TPO | General, Session, Row / Scaling, POC, Value Area, Marks, Summary | | Delta, CVD | Display, Trade size, Average | | Bar Stats | General, Stats | | Net Positioning | General, Absorption | | Bubbles | Display, Filter | Then, where they apply: - **Price marker** (pane studies): the current value on the scale (on by default), in the pane at its right edge (or the study's name badge, "CVD BINANCEF"), a dashed line across the pane at it with its width, and the bar's countdown under the scale's tag. The order-flow panes (Delta, CVD candles and histogram, Open Interest, Liquidations) draw all of it. - **Style**: the colour, width, line style and visibility of each plot, and for pane studies the **Pane** (its own, shared with the pane above, or over the price pane), **Show legend** (the pane's status line) and **Scale padding** top and bottom (room above and below its values, Auto: 8 %). - **Aggregate** (order-flow layers): the markets the layer reads, see [Markets of an indicator](/docs/app/heatmap#markets). - **Defaults** resets to the catalog values. **Cancel** restores what you had. **OK** keeps the changes. Edits apply to the chart live while the dialog is open. ## The legend and panes Each study gets a row in the chart's legend with its short name, inputs and current values. Hover the row for show or hide, settings and remove; on a touch screen, tap the row (it shows three dots) for a menu with the same actions. A script's row also has **Source code** (`{}`, also in its right-click menu): it opens the script in the editor: yours to edit, one from the community library read only, with **Copy to my scripts** (see [The script editor](/docs/scripting/getting-started/script-editor#community-scripts)). Community scripts on a chart follow their author's new versions. Pane studies stack below the price pane. Drag the separators to resize them, and drag or scroll a pane's value axis to stretch its scale. Studies compute only when their inputs or the bars change. The calculations are pure array maths with unit tests. ## Writing your own Write your own indicators and strategies in [Flowscope Script](/docs/scripting): open the script editor from the bottom bar or **Indicators › My scripts › New script**. The [AI assistant](/docs/app/ai-assistant) can write one from a description. ## Sharing a pane Two pane studies can draw in one pane, as Delta Volume and CVD often do: right-click the lower study's legend row and choose **Move to › The pane above** (or set **Pane** to "Shared with the pane above" on its Style tab). It keeps its own scale and value tags; its legend row sits under the first. **Move to › A new pane** splits them again. **Pin to the pane's scale**, in the same menu, draws a shared study on the scale of the pane's own study instead of its own, so the two compare by value (two CVDs, an oscillator and its average). **Its own scale** undoes it. ## Arranging panes Move the pointer over a study pane and five buttons appear at its top right; the legend row's right-click menu has the same actions (on a touch screen, tap the row). | Button | Does | |---|---| | Maximize | Shows the pane large: the others fold to their names and the chart keeps a quarter. Again restores them. It is a view, not saved with the layout | | Collapse | Folds the pane to its legend row; again unfolds it. Saved with the layout | | Up, Down | Moves the pane above or below its neighbour, with the studies sharing it | | Remove | Removes the study | Drag the line between two panes to resize them (not while one is maximized). ## Personal settings templates The **Template** menu at the bottom left of Chart settings, Footprint settings, indicator settings, script settings and order-flow pane settings saves and loads named settings for your account. Chart settings templates save colors, canvas and scales; the chart keeps its asset, interval, drawings, indicators and camera. Indicator templates include all numeric inputs, plot colors and styles, levels, fills, price markers, timeframe, comparison market and Aggregate source. This also covers every TPO section, including sessions, rows, rays and its volume profile. Global row values are included where the indicator uses them. Order-flow pane templates include their Aggregate markets; script templates include all editable inputs, visibility and strategy properties. The selected settings template is saved on the chart, indicator, script or pane and survives layout reloads. Its menu shows a check mark and **modified** when the current settings differ. Values are stored in the layout itself, so deleting a personal template never removes settings from an existing chart. Loading applies live; **Cancel** restores the settings from when the dialog opened. Saving a named template is explicit and survives canceling the chart dialog. In the indicator search, **Up/Down** selects a result and **Enter** adds it. Bar Stats uses the same draggable separators as other lower indicator panes. Compared prices have their own settings for lines, candles or bars, colors, visibility and normalization. ## Price market and indicator sources The asset picker lists the markets captured by the connected hub. Choose the price venue there. Aggregation belongs to each indicator's **Markets** setting and the Footprint settings **Aggregate** page: a Binance Futures BTCUSDT chart can retain Binance candles while its footprint, heatmap, profiles or computed indicators read selected venues together. Script subscriptions can read other markets exposed by that hub. ## Linked charts A link group synchronizes the selected asset. Optional settings also synchronize the interval and drawings. With crosshair synchronization and group restriction enabled, two charts in group 1 exchange crosshair time and price; a chart in group 2 is separate. Price synchronization requires the same coin. Turning off group restriction sends crosshair time across all charts. Panning and zooming remain each chart's own saved view. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/indicators --- # Drawings > Drawing tools, the magnet, locking and the anchored VWAP. Drawing tools live in the rail on the left of the window. Drawings are anchored in time and price, so they stay in place when you change the interval or the chart type. The tools draw on the price pane. A layout can also bring drawings that lie in a study pane, on its values (Market Monkey layouts place some there, such as an arrow in a delta pane); they are drawn there and removed with the chart's drawings, but not edited. ## Tools Tools come in groups. A group with several tools has a flyout (the arrow at its right edge, a right click, or a long press on a touch screen); on a phone the tools are under **More › Drawing tools…**; the rail shows the tool you used last in each group. Hover a tool to see its keyboard shortcut. When the window is short, scroll the rail with the wheel. | Group | Tools | Anchors | |---|---|---| | Cursors | Crosshair, Arrow | — | | Lines | Trend line, Ray, Extended line, Parallel channel | 2 (channel: 3) | | Levels | Horizontal line, Horizontal ray, Vertical line, Cross line | 1 | | Fibonacci | Fib retracement | 2 | | Anchored VWAP | Anchored VWAP | 1 | | Shapes | Rectangle | 2 | | Brushes | Brush, Highlighter | drag freehand | | Text | Text | 1 | | Positions | Long position, Short position | 2, plus a stop handle | | Measure | Price range, Date range, Date and price range | 2 | Below the groups: | Rail button | What it does | |---|---| | Measure | Measure once; you can also Shift+drag on any chart | | Zoom in | Drag a box to zoom into it | | Magnet | Snap anchors to the open, high, low or close of the nearest bar | | Stay in drawing mode (pencil with a lock) | Keep the tool after placing a drawing instead of going back to the crosshair | | Lock all drawings | No drawing can be moved while on | | Hide all drawings | Hide every drawing while on | | Remove drawings on this chart | Delete the active chart's drawings for its market | ## Placing a drawing 1. Pick a tool in the rail. 2. Click on the chart for the first anchor. 3. For tools with more anchors, click again for each one. You can also press at the first anchor and drag to the second. One-anchor tools finish on the first click. The **brush** and the **highlighter** draw while you drag; the chart does not pan with them. After placing, Flowscope returns to the crosshair unless **Stay in drawing mode** is on. Esc cancels a drawing in progress and leaves the tool. ## Editing - **Select** a drawing by clicking it. A floating style bar appears at the bottom of the chart with colour, width, dashed line, lock and remove; text drawings also get a text field. Drag the bar by its grip to move it. - **Move** a drawing by dragging its body. - **Reshape** by dragging a handle. With the magnet on, the handle snaps to the bar's OHLC. - **Delete** the selected drawing with Delete or Backspace, or from the style bar. - **Lock** a single drawing from its style bar or the object tree. The object tree in the right panel lists every drawing on the active chart with show or hide, lock and remove buttons. New drawings default to a 2 px blue line. ## Keyboard shortcuts | Keys | Tool | |---|---| | Alt+T | Trend line | | Alt+H / Alt+J | Horizontal line / horizontal ray | | Alt+V / Alt+C | Vertical line / cross line | | Alt+F | Fib retracement | | Alt+Shift+R | Rectangle | | Alt+B | Brush | | Alt+Shift+T | Text | | Alt+Shift+L / Alt+Shift+S | Long / short position | ## Fib retracement The retracement draws levels at 0, 0.236, 0.382, 0.5, 0.618, 0.786 and 1 between its two anchors, each level in its own colour. ## Anchored VWAP The anchored VWAP draws a volume-weighted average price from the bar you click onwards. Its anchor snaps to a bar. From the style bar you can add deviation bands at ±1σ, ±2σ and ±3σ. Because the curve depends on the bars, the chart computes it, caches it per drawing and recomputes it when history arrives. ## Positions **Long position** and **Short position** draw a position box with entry, target and stop, and label the reward-to-risk ratio (`R:R`). Use them to plan a trade visually; they do not place orders. ## Where drawings are saved Drawings are saved with the chart in the layout, per market. If you switch a chart to another market, its drawings for the first market stay stored and reappear when you switch back. Drawings are part of undo and redo. > [!NOTE] > More drawing tools (pitchfork, Gann) are on the roadmap. ## Editing and snapping The magnet in the left toolbar snaps drawing anchors to the nearest open, high, low or close of the candle under the pointer. A marker shows the snapped target while drawing. Drag a handle to move one anchor, or the drawing body to move it. Fixed range profiles can be selected on the visible profile as well as their anchors. Double-click a drawing to edit its appearance, lock and anchor prices. Price alert lines can be dragged to a new tick-aligned price. Double-click the line to open its alert settings. Telegram and Discord destinations are configured in the notification settings and delivered through the authenticated server. The destination credentials stay on this device. Alert evaluation currently requires an open terminal; server delivery does not create an offline alert engine. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/drawings --- # Alerts > Price, line, indicator, large-trade and liquidation alerts, with a notification and a sound, and on the server with the app closed. An alert watches a market and tells you when something happens: a notification in the app and, if you like, a sound. ## Create one | From | Alert | |---|---| | Right-click a chart at a price, **Add alert at …** | The price crossing that level | | A selected trend or horizontal line, the bell in its toolbar | The price crossing the line | | An indicator's legend row, **Add alert…** | A line of the indicator crossing a value, the price or another of its lines | | A watchlist row, **Add alert…** | The price crossing a level on that market | | The **Alerts** panel (bell in the right rail), **+** | Any of these on the active market, including large trades and liquidations | On a phone, press and hold the chart at a price instead of right-clicking. ## Conditions | Condition | Fires when | |---|---| | Price crossing | The last price crosses the level, either way | | Price crossing up, crossing down | Only in that direction | | Line | The last price crosses a drawn line, as far as the line reaches | | Indicator | The indicator line crosses its target on the newest bar of the chosen interval | | Trade of at least (USD) | A single print, or a taker order's prints within one millisecond, reaches that notional | | Liquidation of at least (USD) | A liquidation of at least that notional | Indicator alerts work with indicators computed from bars (moving averages, RSI, MACD, bands and the like), not with order-flow studies. **Trigger** is **Once** (the alert switches off after firing) or **Every time**. An alert that fires every time has a **Cooldown** (none, 10 seconds, 1, 5 or 15 minutes, 1 hour): after firing it stays quiet that long, so a price wobbling around a level does not ring every second. **Sound** is Chime, Ping, Bell, Alarm or none; the play button beside it previews the sound. Sounds can be turned off for all alerts in **Settings › General**. ### Message templates The **Message** is added after the description (`BTCUSDT crossing 85,300 · your message`). Once it names a placeholder it becomes the whole text instead: | Placeholder | Becomes | |---|---| | `{symbol}`, `{venue}` | The market, for example `BTCUSDT` and `Binance Futures` | | `{price}` | The last price when it fired | | `{alert}` | What happened: `BTCUSDT crossing 85,300` | | `{message}` | The message itself (for webhook bodies) | | `{count}` | How many times the alert has fired, this time included | | `{time}`, `{ts}` | When: `14:05:09` (UTC), or milliseconds since 1970 | For example `{symbol} broke {price} ({count}×)` shows `BTCUSDT broke 85,301.5 (3×)`. ## Where alerts run In the app, alerts are checked every frame against the live data of their markets; the app keeps those markets open while an alert watches them. Signed in, price, line, large-trade and liquidation alerts also run **on the server**, and reach you with Flowscope closed: on Telegram, Discord, a webhook or by mail. They are mirrored to your account as you create, change or remove them. Indicator alerts and script alerts compute the app's indicators and run only while Flowscope is open. The server holds an alert's count and state: an alert that fires once is switched off there, everywhere; one that fires every time keeps its cooldown, also when the server restarts. When the server delivers an alert, the open app still shows it (notification, sound, history) but does not send it a second time. [Flowscope Script](/docs/scripting) raises its own alerts with `alert`; they arrive as the same notifications, from the open app. ## Sending alerts with the app closed The **Alerts** panel's **Server** tab sets where the server sends your alerts: | Field | What it sends | |---|---| | Telegram bot token and chat ID | The alert's text to a chat through your bot (`sendMessage`); both are needed | | Discord webhook | The text to the channel of a Discord webhook (Channel settings › Integrations › Webhooks) | | Webhook URL | A JSON post: `{"text": …, "ts": …, "source": "flowscope"}`, or your own **Webhook body** with the placeholders above and `{text}` (values escaped for JSON) | | Mail | The text to your account's address | Tokens and webhook addresses are stored encrypted and never shown again: the panel says which are set; type a new one to replace it, or remove it. **Send a test** sends one through every destination and shows what each answered. The tab also lists what the server fired lately and where each went. Each account has up to 100 server alerts and 60 notifications an hour; what fires beyond that is listed, not sent. ## Sending alerts from the open app Settings › General › Notifications sends what fires in this window to Discord, Telegram or a webhook as well, while Flowscope runs (the fields above; credentials stay on this device). Use it for indicator and script alerts, or without an account. Only `https://` addresses are used; **Send a test alert** fires one through all of them. A failed delivery is logged by host and status only, because webhook addresses contain their secrets. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/alerts --- # Bar replay > Play a chart forward from a past bar to practise reading order flow and test ideas without hindsight. Bar replay hides everything after a bar you pick and then plays the chart forward from there, bar by bar, as if the market were trading again. It works on every chart type, interval and market, with your studies, scripts and drawings. ## Start a replay 1. Click **Replay** in the top bar. It applies to the active chart. 2. A blue line follows the pointer over the chart. Click the bar to start from. Esc cancels. 3. Everything after that bar disappears and the replay controls appear at the bottom of the chart. Click **Replay** again, or × on the controls, to go back to the live chart. ## The controls | Control | What it does | |---|---| | ⋮⋮ | Drag the controls anywhere on the chart | | **Start bar** | Pick another start bar; Esc returns to the running replay | | Play / Pause | Play the chart forward at the chosen speed | | Forward | Show the next bar | | Speed | 0.1x to 10x; 1x is one bar per second | | Date | Open time of the newest bar shown | | Bars left | Bars between the replay and now | | × | Leave the replay, back to real time | Keys, with the pointer over the chart: Shift+→ shows the next bar, Shift+↓ plays and pauses. Playback stops at the newest bar. You can change the interval while replaying: the replay keeps its time and shows the new interval's bars up to it. ## What a replay shows - **Bars, footprints, studies and profiles** are computed only from the bars up to the replay bar, so nothing reveals what comes next. - **Scripts and strategies** run on the replayed bars; a strategy's backtest and the [Strategy Tester](/docs/scripting/strategies/strategy-tester) stop at the replay bar and grow as it plays. - **Heatmap, bubbles and liquidations** are drawn up to the replay bar and hidden after it. - **Live-only layers are off:** the book profile, the trade pulse and the bar countdown. The price tag on the axis shows the close of the replay bar. > [!NOTE] > Replay reads the history the chart has loaded. Scroll left before you start to > load older bars. Footprints and heatmap further back come from what the data > archive holds ([Data](/docs/data)). Source: https://flowscope.91-99-118-129.sslip.io/docs/app/replay --- # AI assistant > Ask about the chart, build indicators and strategies, and research them with the quant lab. The assistant lives in the panel on the left (Alt+A). It sees the chart you are on, reads the market, writes Flowscope Script, puts indicators and strategies on the chart and tests them. Connect it once with a Claude or ChatGPT plan, an API key or any OpenAI-compatible server; the connection stays on your device. For DeepSeek, open **Connect your AI**, choose **DeepSeek → Add an API key**, paste your key and click **Connect**. Flowscope checks the key and loads the available models automatically; no base URL is needed. It never places, changes or cancels orders and never moves funds. ## What it can do | Area | Tools it uses | |---|---| | Understand the chart | the chart's market, interval, indicators, scripts, drawings and position; a picture of the chart | | Read the market | bars of any market and interval, the order book, large trades, liquidations, funding, open interest | | Write scripts | the scripting docs and built-ins, a compiler check, putting the script on the chart (typed into the script editor as you watch, saved under My scripts) | | Test strategies | the Strategy Tester report, over the last days or a date range | | Change the chart | add or remove built-in indicators, switch market, interval or chart type, draw levels and notes | | Research (quant lab) | the tools below | ## The quant lab Research that would take the assistant dozens of steps and long lists of bars runs inside Flowscope in one step, in parallel on all cores, and comes back as a short summary. Answers are faster, and a conversation uses far fewer tokens. | Tool | What it answers | |---|---| | **Market profile** (`market_stats`) | How a market moves: volatility, the size of a typical bar, fat tails, whether moves continue or revert (autocorrelation, variance ratios, Hurst), how efficient trends are, which UTC hours and weekdays move and trade most, the biggest bars. | | **Signal study** (`signal_study`) | Whether an idea has an edge before a strategy is built: what the price did 1, 3, 5, 10 and 20 bars after a signal, against every bar, with hit rate, t-statistic and the best and worst excursion on the way. | | **Optimize** (`optimize`) | The best inputs of a strategy: every combination (or a fixed random sample when there are too many), each a full backtest. The search only sees part of the bars; its best runs are then tested on bars it never saw (holdout), or the choice is repeated fold by fold on the past only (walk-forward). It reports how much each input matters and whether the best inputs sit on a plateau (their neighbours do almost as well) or a spike (likely overfit). | | **Monte Carlo** (`monte_carlo`) | How much of a backtest was the luck of the order: the strategy's trades resampled thousands of times, with the range of returns and drawdowns, the chance of a loss and of a deep drawdown, and the longest losing streak to expect. | | **Compare markets** (`compare_markets`) | Whether the edge is general: the same strategy and inputs on several markets and intervals at once. | Each study runs on the bars of the chart's market and interval unless you name others, over the strategy's test period or the days you ask for; it loads older history first if the period needs it. A strategy runs exactly as on the chart, with its inputs and its properties (capital, sizing, commission, slippage, margin). Ask in plain words, for example: - "Profile ETH on 15 minutes over the last 30 days." - "Is there an edge in buying when RSI drops below 25 on BTC 1h?" - "Optimize the fast and slow length of my strategy with walk-forward, then run a Monte Carlo on the result." - "Does this strategy also work on SOL and ETH, on 1h and 4h?" With "apply the best inputs" the assistant writes the chosen values into the strategy's inputs on the chart; you see the Strategy Tester update. ### Reading the results - **Out of sample counts.** A search will always find something that looked good in the past. Trust inputs that also hold on the held-out bars, on neighbouring values and on related markets. - **Few trades say little.** Runs with fewer trades than the minimum (20 by default) are not ranked. - **Margin calls.** Sizing at or near 100 % of equity with costs makes the backtester close small parts of the position for want of margin, as TradingView does. Each of those counts as a trade, so the trade count rises and the win rate sinks. The reports say when this happened; size at most about 95 % of equity for clean figures. - Results are measurements of the past, not advice or a forecast. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/ai-assistant --- # History and data > What Flowscope pulls from the venues, how the data hub records and serves it, and what the app keeps on the machine. Every market and its history come from the Flowscope data hub. The hub talks to each venue once for every client and records every market's live stream around the clock: trades, the order book, open interest, funding and liquidations. Everything else is built from that recording: bars of every timeframe, footprints, delta and CVD, the heatmap. Nothing is taken from a venue's candles, trade pages, funding history or open-interest history, so a chart's history begins where the hub's recording does. The app itself keeps no market data on the machine. ## What Flowscope pulls | Data | Live | History | |---|---|---| | Order book | Snapshot plus every change, sequence-checked | Each price bucket's resting size at the close of every minute, as the hub recorded it. No venue publishes historical order books | | Trades | Every print | What the hub recorded: footprints, and its last 30 minutes of trades to seed the time and sales | | Candles | — | Built by the hub from the trades it recorded, for any whole number of minutes | | Open interest | Websocket (Bybit, OKX, Hyperliquid), every 15 s over REST (Binance Futures) | What the hub recorded, in 5-minute or hourly points | | Funding | Websocket | The rate the hub recorded just before each settlement, on the market's own period as the venue's stream gives it (Bybit and OKX at once, Binance from its first settlement); until known, the venue's usual one: 8 hours, 1 hour on Hyperliquid and Deribit | | Liquidations | Websocket | Per minute, as the hub recorded them | | Hyperliquid accounts | Positions and trigger orders, read on a budget | From the hub | | Option chains (BTC, ETH on Deribit) | The newest chain the hub took, asked for once a minute, for [gamma exposure](/docs/app/gamma-exposure) | Recorded by the hub once a minute; the app shows only the newest | ## How history is filled Charts ask for what they show every frame: candles back to the left edge, and footprints and heatmap for the visible range. All of it comes from the data hub. The browser app reaches it through the website at `/hub`, or another hub through `?hub=`. See [The data hub](#hub). - **Candles** are built by the hub from what it recorded. Bars longer than a minute load as candles of their own length, so a 4-hour or daily chart is full with its first request. Candles load three pages at a time, computed by time, so scrolling back never waits on a chain of requests. - **Footprints** load as the hub's day segments, whole days, **newest first**. - **Heatmap** hours load as the hub's tiles, at the zoom's resolution. Arriving history patches the affected bars in place. Before the hub's recording begins, a chart has no bars; a minute nobody recorded is **Unavailable**, never filled from elsewhere. A strategy's test period loads its bars back to its start. ## What is kept | Store | Holds | Resolution | |---|---|---| | Raw tape | Every print: time, price, size, side | Exact | | Liquidity runs | Each price bucket's resting size at the close of every minute | Bucket ≈ 0.001 % of price, one minute | | Minute footprints | Per minute: OHLC, buy and sell volume, trade counts, size bands, and buy/sell volume per row | Row ≈ 0.005 % of price | | Minute context | Open interest, funding, long and short liquidations, book depth within ±0.5 / 1 / 2 % | Per minute | | Candles | The hub's candles, built from its recorded minutes | Per minute, or the bar's length | A busy BTC perpetual takes about 0.2 MB of minute footprints per day. The book is recorded once a minute, by choice: the minute that is forming shows the live book, and once it closes it keeps what stood at its close. An order that came and went within a minute, such as the one that sat for 400 ms or the wall pulled and re-posted, is not in the record, and on bars shorter than a minute the heatmap moves in minute steps. In the app these stores live in memory for the session. The hub serves them from what Derive prepared; see [Files](/docs/data/storage). ## On this machine {#local-persistence} The app keeps no market history on the machine. It holds what it shows in memory for the session, and the next session loads it from the hub again. The browser keeps layouts, preferences and the app's other settings in `localStorage`. See [Layouts](/docs/app/layouts#what-is-stored-where). ## Memory Open markets share a 768 MB memory budget in the browser. Each market's share is split into 30 % raw tape, 50 % liquidity runs and 12 % minute footprints. The oldest days leave memory first and load from the hub again when a chart reaches them. Candles beyond a quarter of the footprint share (at least three days) that no chart shows leave memory too. ## The data hub {#hub} The data hub (`flowscope-hub`) serves every market over one websocket, plus REST, from one source: the raw archive Capture writes and the history Derive prepares from it ([Data](/docs/data)). It never asks a venue for anything. Signed trading requests from your own live accounts go straight to the venue. The browser app reaches the hub through its own website at `/hub`; the site passes it on with its own key, so the browser holds none. `?hub=` in the page URL picks another hub; `?hub=` alone selects the default. When the hub cannot be reached, every pane says "Data hub unreachable" and reconnects. There is no mode on the venues directly; the [simulator](/docs/app/install#simulator) runs without a hub. The source is not a setting. **Settings › Data › Market data** shows it: the hub's address and **Connected** or **Reconnecting**, or **The simulator**. With a hub: - For every day a chart wants, the market asks the hub, up to four days in flight per market. - Today's segment keeps growing on the hub, so it is asked again every two minutes. - A day the hub does not have is asked again after ten minutes. - Heatmap hours before the session's own runs load from its tiles, at the zoom's resolution. - Hyperliquid maps and positioning footprints load from the hub's `/v1/hl/` endpoints for the coins it serves; other coins have none. A trailing slash on the URL is fine. ## Known limits - History exists only where the hub recorded it: before its recording a chart has no bars, no footprints and no heatmap. - The book is recorded once a minute; what came and went within a minute is not in the history. - The hub records the open hour live only for the markets it is configured to record; others come from the hourly archive once the hour is sealed. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/history-and-data --- # Performance > How Flowscope stays fast with many charts, the measured numbers, and the performance overlay. Flowscope is built to keep sixty charts open, each with its own heatmap and order flow, without the interface thread doing market work. Markets are processed in parallel on a worker pool; the interface only reads and paints. ## How the work is split | Where | What runs there | |---|---| | Network callbacks | Websocket decoding and routing by symbol | | Engine (8 ms a frame) | One actor per market: book, liquidity runs, tape, bars, footprints, context. Heatmap rasters run on their own worker. | | Interface thread | Reads each pane's data under a short lock and paints | Each market actor drains up to 512 messages, applies them under one write lock, and does follow-up work (snapshots, history paging, series rebuilds) outside the lock. Idle workers park until their mailbox wakes them. In the browser there are no threads. The engine runs actors and jobs for up to 8 ms per frame, and network callbacks feed the same mailboxes. A chart converts only new bars each frame and recomputes studies only when their inputs or the bars changed. Costly aggregates of live data (profiles, bubbles, the book) refresh a few times a second. ## Measured Simulator, 60 charts with heatmap, bubbles and book profile over 40 markets, release build under Xvfb with software OpenGL (llvmpipe): | Measure | Value | |---|---| | Events processed | ~4 200 / s | | Market memory | 137 MB | | Process RSS | ~430 MB | | Interface CPU per frame | 6.1 ms average, 9.3 ms max | | Heatmap rasters | 90 / s | | Engine worker CPU | Near zero | The frame rate in that run, 11 fps, was bound by software rasterization. On a GPU that cost moves off the CPU. Live, with BTC on all five venues plus ETH and books running, the probe runs saw 0 decode errors and 0 reconnects. ## Run the load test ```bash make serve # build, then serve web/dist on http://localhost:8080 ``` Then open `http://localhost:8080/?sim`: the browser app on the simulator. Open the performance overlay with the pulse icon in the top bar, or add `&perf=1` to the address. Load-test layouts open with `&preset=stress` (60 charts) or `&preset=stress-studies` (100 charts with 10 indicators each), e.g. `http://localhost:8080/?sim&preset=stress-studies&perf=1`. ## The performance overlay Toggle it with the pulse icon in the top bar. Values are measured over rolling one-second windows. | Row | Meaning | |---|---| | Frame rate | Frames painted per second | | UI frame time | Interface CPU per frame, average and maximum | | Panes | Panes in the layout | | Heatmap rasters | Raster jobs finished per second | | Markets | Markets open in the engine | | Market memory | Memory held by all markets | | Events | Market events processed per second | | Workers | Engine worker threads | | Actors | Tasks running on the workers | | Queued jobs | Background jobs waiting | | Connections | Open websocket connections | | Frames | Websocket frames received per second | | Bandwidth | Network bytes received per second | | Reconnects | Reconnects since start | | Decode errors | Messages that failed to decode | The bottom bar shows a short summary at all times: markets open, market memory, events per second and frame rate. > [!TIP] > Rising **Queued jobs** means the workers cannot keep up. Rising **Reconnects** or **Decode errors** points at the network or a venue, not at Flowscope's rendering. ## Memory The engine has a fixed budget, 768 MB, divided among the open markets. When a market reaches its share, the oldest chunks leave memory first. Footprint days load from the data hub again when needed. See [History and data](/docs/app/history-and-data#memory). ## Tips - Use the release browser build (`make web`) for real work. `make web-dev` is a faster, unoptimized build. - Maximizing a pane (double-click its header) draws that pane alone until you restore it. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/performance --- # Settings > Page parameters, preferences and built-in defaults. Flowscope is configured in the interface and saved with your layouts and preferences. A few switches are page parameters, read once when the page loads. ## Page parameters | Parameter | Values | Effect | |---|---|---| | `?sim` | — | Run on the built-in simulator instead of the data hub | | `?hub=` | A URL, for example `http://127.0.0.1:8790` | Take every market and its history from this data hub; `?hub=` alone selects the default, the website's own `/hub` | | `?preset=` | `orderflow`, `liquidity`, `venues`, `chart`, `stress`, `stress-studies`, `flow` | Start from this preset instead of the saved layout | | `?perf=1` | — | Open the performance overlay at start | Parameters combine: `?sim&preset=stress-studies&perf=1`. ### Presets | `FLOWSCOPE_PRESET` | Preset | |---|---| | `orderflow` | Orderflow desk | | `liquidity` | Liquidity | | `venues` | BTC on every venue | | `stress` | 60 charts (load test) | | `stress-studies` | 100 charts with 10 indicators each (load test) | | `flow` | Flow studies: a developer layout with every flow study, not offered in the menu | For the regular starting chart and included community layouts, see [Layouts](/docs/app/layouts#starting-a-layout). A preset from the page URL is shown instead of the restored layout. The open layout keeps its name, and with autosave on, what is on screen is saved to it as usual. ### Examples ```bash # Simulator: serve the browser build, then open http://localhost:8080/?sim make serve # Live, every market and its history from a hub on another machine: # append ?hub=https://hub.example.com to the page URL ``` ## Preferences Preferences outlive layouts and apply everywhere. They are edited in the interface. | Preference | Where to change it | Default | |---|---|---| | Favourite intervals | Stars in the interval menu | `1m`, `5m`, `15m`, `1h`, `4h`, `1D` | | Custom intervals | **Add custom interval…** | None | | Favourite chart types | Stars in the chart type menu | Candles, Footprint, Liquidity | | Favourite studies | Stars in the indicator dialog | Liquidity Heatmap, CVD, Volume Profile, VWAP | | Indicator templates | Templates button in the top bar | None | | Time zone | Clock in the bottom bar | UTC | | Autosave | Layout menu | On | | Magnet | Tool rail | Off | | Stay in drawing mode | Tool rail | Off | | Recent symbols | Remembered from symbol search | The last 12 | | Crosshair sync across charts | Settings › General › Charts | On | | The crosshair of a linked chart stays in its link group | Settings › General › Charts | Off | | Linked charts share drawings | Settings › General › Charts | On | | Theme: Dark, Midnight (blue-tinted), Graphite (lighter grey) or Black (OLED) | Settings › General › Interface; applies at once | Dark | | Interface scale | Settings › General › Interface | 100 % | ### Time zones The clock menu offers UTC, local time and fixed offsets: Los Angeles (UTC−8), New York (UTC−5), Berlin (UTC+1), Berlin summer (UTC+2), Dubai (UTC+4), Singapore (UTC+8) and Tokyo (UTC+9). Fixed offsets do not follow daylight saving. **Local time** follows your machine and is re-read every minute. Trading sessions in [profiles](/docs/app/profiles#sessions) follow their own exchange clocks regardless of this setting. ## Per-chart settings Saved with the layout. See [Charts](/docs/app/charts#chart-settings). | Setting | Default | |---|---| | Chart type | Depends on the pane: candles, footprint or liquidity | | Footprint cells | Bid × Ask | | Up, down and wick colours | `#22AB94` up, `#F23645` down | | Line colour | `#2962FF` | | Logarithmic scale | Off | | Last price line, countdown to bar close | On | | Grid | On | | Watermark | Off | | Legend: OHLC, indicator titles, indicator values | On | Ladder and depth book settings are listed in [DOM and tape](/docs/app/dom-and-tape). ## Built-in defaults These are fixed in the engine and not exposed as settings. | Default | Value | |---|---| | Memory budget for markets | 768 MB | | One-minute candles loaded at start | 3 days, from the data hub | | Raw tape seeded at start | The hub's last 30 minutes of trades | | Footprint and heatmap history | What the hub serves from the archive | | Engine time per frame | 8 ms | ## Where settings are stored | What | Where | |---|---| | Layouts, preferences, side panel | `localStorage` of the page's origin, and your account when signed in | | Market history | Not stored: it comes from the data hub each session | | Snapshots | The clipboard | See [Layouts](/docs/app/layouts#what-is-stored-where) for the storage keys and [History and data](/docs/app/history-and-data#local-persistence) for what the app keeps on the machine. Source: https://flowscope.91-99-118-129.sslip.io/docs/app/settings --- # Flowscope Script > The typed language for writing your own indicators and strategies inside the Flowscope terminal. Flowscope Script is the language for your own indicators and strategies in the terminal. A script states up front which market data it needs, which settings it exposes and what it draws, then reacts to events as the data arrives. Strategies add a TradingView-style broker emulator and the Strategy Tester. The same script runs over the chart's loaded history and keeps going as new bars arrive. You never write a separate code path for the live edge. ## What you can build - **Indicators** that draw lines, histograms, candles, markers and fills on the price pane or in their own panes. - **Order-flow studies** from volume delta, CVD, open interest, liquidations, funding and individual trades, on one venue or an aggregated market. - **Strategies** that place simulated orders, backtest over the chart's history and report in the Strategy Tester. - **Alerts** with typed payloads that fire on live data. Write scripts in the script editor (**Script editor** in the bottom bar) and put them on the active chart with **Add to chart**. ## A first script This script plots volume delta as a histogram coloured by sign, with a smoothed line over it: ```flowscope title="delta-histogram.fs" script "Delta histogram" input smoothing = input.int(9, title: "Smoothing", min: 1, max: 50) data chart = subscribe(data.ohlcv) pane deltaPane = pane(title: "Delta", height: 0.25) plot ( bars = plot.histogram(title: "Delta", on: deltaPane) trend = plot.line(title: "Smoothed", color: color.amber, width: 2, on: deltaPane) ) on chart.close { let delta = chart.delta() let shade = (delta ?? 0.0) >= 0.0 ? color.up : color.down bars.plot(delta, color: shade) trend.plot(ta.ema(delta, smoothing)) } ``` Read it from top to bottom: 1. `script "Delta histogram"` names the script; the name appears in the legend. 2. `input` declares a setting you can change in the script settings. 3. `data chart = subscribe(data.ohlcv)` subscribes to the chart's own bars. 4. `pane` and `plot` declare where the output goes and how it looks. 5. `on chart.close` runs once for every confirmed bar and feeds both plots. `chart.delta()` returns `null` when a bar has no volume split. The histogram shows a gap for that bar and `ta.ema` carries on with the next value. Missing data is always explicit; see [missing values](/docs/scripting/guides/missing-values). > [!TIP] > Put the script on an aggregated market, such as BTC on all venues. The delta then > covers every venue at once. ## Chart context A script runs on a chart. The `context` values describe it and stay fixed for a run: | Value | Type | Meaning | |---|---|---| | `context.exchange` | `string` | Venue id: `"binance"`, `"binancef"`, `"bybitf"`, `"okxf"`, `"hyperliquid"`, `"asterf"`, `"lighterf"`, or `"aggregate"`. | | `context.symbol` | `string` | The instrument symbol. | | `context.base` | `string` | Base asset, for example `"BTC"`. | | `context.quote` | `string` | Quote asset, for example `"USDT"`. | | `context.timeframe` | `timeframe` | The chart timeframe. | | `context.tickSize` | `float` | Minimum price increment. | | `context.stepSize` | `float` | Minimum quantity increment (`0.0` when unknown). | Changing the chart's market or interval re-runs the script with the new values. `context` is readable anywhere, including `setup` and `state` initialisers. It is handy for distances in ticks: ```flowscope script "Distance from the average in ticks" input len = input.int(50, title: "Average length", min: 2, max: 500) data chart = subscribe(data.ohlcv) pane ticksPane = pane(title: "Ticks from SMA", height: 0.2) plot ticks = plot.histogram(title: "Ticks", on: ticksPane) on chart.close { let c = chart.close let avg = ta.sma(chart.close, len) if c != null && avg != null { let away = (c - avg) / context.tickSize ticks.plot(away, color: away >= 0.0 ? color.teal : color.purple) } } ``` Timeframes cannot be multiplied directly; convert with `time.tfSeconds`, compute in seconds and convert back with `time.tfFromSeconds`. See [multiple timeframes](/docs/scripting/guides/multiple-timeframes). ## Where to go next ```cards [Start here](/docs/scripting/getting-started) How a script is organised, your first indicator, and a guide for Pine Script users. [Guides](/docs/scripting/guides) Data and events, types, functions, pattern matching, colours, timeframes and repainting. [Strategies & Backtesting](/docs/scripting/strategies) Turn a script into a strategy, place orders and read the Strategy Tester. [Examples](/docs/scripting/examples) Complete scripts for order flow, liquidations, funding, dashboards and strategies. [Performance](/docs/scripting/performance) Keep larger scripts fast. [Language reference](/docs/scripting/reference) Exact syntax, types, null rules and execution model. [Built-in catalog](/docs/scripting/reference/catalog) Every built-in function, constant, data source and record. [Diagnostics](/docs/scripting/reference/diagnostics) Every compiler message with a failing and a corrected script. [llms.txt](/docs/llms.txt) A compact plain-text reference for AI assistants and tools. ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting --- # Start here > How a Flowscope script is organised, what happens when it starts, and when each handler runs. Every Flowscope script has two kinds of content. Declarations describe what the script contains: its settings, its data, its outputs and its reusable logic. Handlers describe what happens when market data changes. You do not need the whole language to build something useful. If you are new, read this page and then [build your first indicator](/docs/scripting/getting-started/first-indicator). If you already write TradingView Pine Script, start with [coming from Pine Script](/docs/scripting/getting-started/coming-from-pine). Pick up the [guides](/docs/scripting/guides) as individual topics become relevant. ## How a script is organised A script is a single file. The first line is the header `script "Name"`. Every line after it belongs to a top-level declaration. Loose statements between declarations are not allowed; executable code lives inside a function or a handler. | Declaration | What it defines | |---|---| | `input` | A setting shown in the script's settings panel. | | `data` | A market-data subscription and the name you read it by. | | `state` | A value that is initialised once and kept across all handler calls. | | `setup` | A value computed once at start, or a family of values whose shape is fixed at start. | | `pane` | A sub-pane below the price chart. | | `window` | A separate window with its own panels: charts, tables or heatmaps. | | `plot` | A visual output, such as a line or a histogram, that handlers feed. | | `fill` | A shaded area between two plots. | | `alert` | A named alert that handlers can trigger on live data. | | `strategy` | Turns the script into a strategy and sets backtest properties. See [strategies](/docs/scripting/strategies). | | `type` | A record with named fields and optional methods. | | `fn` | A reusable calculation or action. It runs only when called. | | `on` | A handler that runs for one event of one subscription. | ### Grouped declarations `input`, `data`, `state`, `setup`, `plot`, `fill`, `pane` and `alert` have a grouped form. Put one declaration per line inside parentheses: ```flowscope input ( length = input.int(20, title: "Length", min: 1) upColor = input.color(color.up, title: "Up") downColor = input.color(color.down, title: "Down", sameLine: true) ) ``` This is exactly the same as writing three separate `input` lines. Grouping only helps you keep related declarations together. `script`, `strategy`, `type`, `fn`, `window` and `on` are always written on their own. > [!NOTE] > One statement goes on one line, and there are no semicolons. To split a long expression, break the line after an operator such as `+`, `&&` or `?`. ## A complete script shape The script below draws the previous four-hour range on any chart. It uses most kinds of declaration at least once. ```flowscope title="previous-4h-range.fs" script "Previous 4h range" type Range { high: float low: float fn mid() -> float { return (self.high + self.low) / 2.0 } fn width() -> float { return self.high - self.low } } fn rangeOf(high: float?, low: float?) -> Range? { if high == null || low == null { return null } return Range { high: high, low: low } } input ( edgeColor = input.color(color.blue, title: "Range edges") showMid = input.bool(true, title: "Show midpoint") ) data ( chart = subscribe(data.ohlcv) h4 = subscribe(data.ohlcv, timeframe: 4h) ) state last: Range? = null plot ( top = plot.line(title: "4h high", color: edgeColor, style: linestyle.step) middle = plot.line(title: "4h mid", color: color.gray, style: linestyle.dotted) bottom = plot.line(title: "4h low", color: edgeColor, style: linestyle.step) ) on h4.close { last = rangeOf(h4.high, h4.low) } on chart.update { let r = last if r != null { top.plot(r.high) middle.plot(showMid ? r.mid() : null) bottom.plot(r.low) } } ``` How the parts fit together: 1. `Range` groups two prices that always travel together and adds two methods. 2. `rangeOf` is a plain function. Declaring it does nothing; it runs only when `on h4.close` calls it. 3. Two inputs let you pick the edge colour and hide the midpoint. 4. Two subscriptions follow the chart's candles and four-hour candles on the same market. 5. `state last` remembers the most recent confirmed four-hour range between handler calls. 6. The three plots declare their default look. 7. `on h4.close` runs once per confirmed four-hour bar and stores the range. 8. `on chart.update` runs on every chart update and draws whatever range is stored. `last` is a state value, which is a field-like storage slot. The handler copies it to the local `r` before the null check, because null checks only narrow locals. The [missing values](/docs/scripting/guides/missing-values) guide explains why. ## What happens at start When you add a script to a chart, Flowscope prepares it in this order: 1. **Compile.** The source is parsed and type-checked. Errors appear with a code you can look up in [diagnostics](/docs/scripting/reference/diagnostics). 2. **Inputs.** Each input takes its saved value, or its default on first use. 3. **Setup.** `setup` expressions, panes, windows, plots, fills and alerts are created. Setup can call setup-safe built-ins and read `context` and inputs, but cannot call your own `fn`s. 4. **State.** Every `state` initialiser runs exactly once. 5. **History.** The loaded history of every subscription is replayed bar by bar, in time order, through your handlers. 6. **Live.** As new data arrives, the chart runs the script again over its history, so the newest bars go through the same handlers. Because history and live data go through the same handlers, an indicator you see on old bars behaves the same way on new ones. If you change an input, the script restarts from step 2 and recomputes history. > [!IMPORTANT] > A script may take at most 10 million steps in one update. A script that exceeds it stops with an error instead of slowing the terminal. See [performance](/docs/scripting/performance). ## Handlers and when they run Period-based subscriptions, `data.ohlcv`, `data.oi`, `data.vd`, `data.cvd` and `data.stat`, support three handlers: | Handler | When it runs | Typical use | |---|---|---| | `on x.open` | Once when a new period of `x` starts. | Reset per-bar counters, start a new zone. | | `on x.update` | Every time `x` changes, including many times while a live bar is forming. | Live values, drawings that follow price. | | `on x.close` | Once when a period of `x` is confirmed. | Final values, confirmed signals, strategy orders. | A `data.trades` subscription uses a single handler, `on t.trade(tr) { }`. It runs once for every print. `tr` is a `Trade` record with `time`, `price`, `size` and `isBuy`. `data.book` and `data.volume` are snapshot sources. They have no handlers of their own; you read them inside a handler on another subscription. A script can have as many handlers as it needs, but only one for each subscription and event pair. Handlers communicate through `state`: one handler writes a value, a later call of another handler reads it. During history replay, a bar triggers `open`, then `update`, then `close`. On a chart, new data re-runs the script over the loaded history, at most every 0.75 s: confirmed bars go through `close`, the forming bar through `open` and `update`. Continue with [your first indicator](/docs/scripting/getting-started/first-indicator), or read [data and update events](/docs/scripting/guides/data-and-events) for the full rules on what each subscription returns inside each handler. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/getting-started --- # The script editor > Write, save, open and organise your scripts, and put them on a chart, in the editor panel on the right. The script editor is a panel at the right of the terminal. **Script editor** in the bottom bar shows and hides it; drag its left edge to make it wider or narrower. ## The toolbar | Part | What it does | |---|---| | Script name ▾ | The script menu: save, copy, rename, create, the scripts you used lately, open, delete. A dot after the name means unsaved changes | | **▷ Add to chart** | Saves and puts the script on the active chart. Opened from a chart's legend it reads **Update on chart** and replaces the code there. Disabled while the code has errors | | Save | Saves to your scripts (Ctrl+S); someone else's script is saved as your own copy | | Share | Share the script with a link or with people (signed in) | | ⋯ | Open a script, show or hide the minimap and the log, the font size | In a narrow panel **Add to chart** shows only its ▷ icon and the minimap steps aside; long lines scroll sideways instead of being cut off. ## The script menu | Item | | |---|---| | Save script | Ctrl+S | | Make a copy | A copy named "… (copy)", opened in the editor | | Rename… | Changes the name in the script's `script "Name"` line | | Create new | An indicator or a strategy from a template, or a script from a share link | | Recently used | The last five scripts you opened | | Open script… | All your scripts (Ctrl+O in the editor) | | Delete script | Asks once more: click again within three seconds. Scripts others shared with you are removed from your list instead | ## Open script **Open script…** lists every script you have, with a search field and filters for indicators, strategies, scripts shared with you and scripts added from the library. To share an indicator with invited people (each until a date), publicly or by a secret link, open it in **Indicators › Library & sharing › Mine** and press the pencil (see [Layouts › Library](/docs/app/layouts#library)). Click a script to open it. Hover a row for its buttons: **+** puts the script on the active chart, the bin deletes it (a second click confirms) without opening it first. ## Your scripts in the indicators dialog The indicators dialog (**Indicators** in the top bar, or /) has **My scripts** under Personal. Click a script there to add it to the chart; **{ }** opens it in the editor. Scripts already on the chart are marked **On chart**, and **New script** starts a new indicator in the editor. ## Community scripts A script from the community library (**Indicators › Community**, the Library, or a script link) goes on the chart without a copy in your scripts, and stays up to date: when its author publishes a new version, your charts switch to it by themselves and keep your input values where the new version still has them. Its settings show **Community · v… · by …**. **Source code** on such a script opens it read only, marked **Community · read only**. **Copy to my scripts** (also in the script menu) makes a copy of your own, named "… (copy)", and opens it: change it and add it to a chart. Your copy no longer follows the library; the community script on the chart stays as it is. Charts with a community script added before this worked this way are linked again on their own when the script there is unchanged; the old copy leaves your scripts once nothing uses it. Copies you changed stay yours. ## Writing code | Keys | | |---|---| | Ctrl+S | Save (and update the chart the script came from) | | Ctrl+O | Open script… | | Ctrl+/ | Comment the selected lines out, or back in | | Enter | New line with the indentation of the last, one level deeper after `{`, `(` or `[` | The code compiles a quarter of a second after you stop typing. Problems appear in a list under the code with their line and column; click one to jump there. The status line counts errors and warnings and shows the cursor's position; **Logs** opens what the script on the chart wrote with `log.info(...)`. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/getting-started/script-editor --- # Build your first indicator > Write a configurable trend band step by step and learn what each line of the script does. In this walkthrough you build a small trend indicator: a moving average with a volatility band around it. You can choose the average type, its length and the band width. The line changes colour with its slope. The finished script is short, but it uses the same structure as every larger script. Each step below adds one piece. ## The finished script ```flowscope title="trend-band.fs" script "Trend band" input ( kind = input.select("EMA", title: "Average", options: ["EMA", "SMA", "HMA"]) length = input.int(34, title: "Length", min: 2, max: 400) bandMult = input.float(1.5, title: "Band width (ATR)", min: 0.0) upColor = input.color(color.up, title: "Rising") downColor = input.color(color.down, title: "Falling", sameLine: true) ) data chart = subscribe(data.ohlcv) plot ( mid = plot.line(title: "Average", width: 2) upper = plot.line(title: "Upper band", color: color.gray) lower = plot.line(title: "Lower band", color: color.gray) ) fill band = fill.between(upper, lower, color: color.withAlpha(color.gray, 24)) on chart.update { let sma = ta.sma(chart.close, length) let hma = ta.hma(chart.close, length) let ema = ta.ema(chart.close, length) let avg = kind == "SMA" ? sma : kind == "HMA" ? hma : ema let atr = chart.atr(length) let rising = ta.rising(avg, 1) ?? false mid.plot(avg, color: rising ? upColor : downColor) if avg != null && atr != null { upper.plot(avg + atr * bandMult) lower.plot(avg - atr * bandMult) } } ``` ## 1. Name the script ```flowscope script "Trend band" ``` Every script starts with this header. The name is what you see in the indicator list and in the chart legend. Pick something you will recognise later. ## 2. Add settings ```flowscope input ( kind = input.select("EMA", title: "Average", options: ["EMA", "SMA", "HMA"]) length = input.int(34, title: "Length", min: 2, max: 400) bandMult = input.float(1.5, title: "Band width (ATR)", min: 0.0) upColor = input.color(color.up, title: "Rising") downColor = input.color(color.down, title: "Falling", sameLine: true) ) ``` Inputs become controls in the script's settings. The first argument is always the default. `min:` and `max:` keep values sensible. `sameLine: true` puts the second colour picker next to the first. Inside the script, each input is a plain value. `length` is an `int`, `kind` is a `string`, `upColor` is a `color`. Inputs are fixed for one run. When you change one, the script restarts and recomputes its history. ## 3. Subscribe to data ```flowscope data chart = subscribe(data.ohlcv) ``` `subscribe(data.ohlcv)` gives you the candles of the chart the script is attached to. Every argument you leave out, such as `exchange:`, `symbol:` and `timeframe:`, is taken from the chart. `chart` is a name you chose. It is not built in. You read candle fields through it: `chart.open`, `chart.high`, `chart.low`, `chart.close`, `chart.volume` and more. All of them are nullable, because a bar can be missing. ## 4. Declare the outputs ```flowscope plot ( mid = plot.line(title: "Average", width: 2) upper = plot.line(title: "Upper band", color: color.gray) lower = plot.line(title: "Lower band", color: color.gray) ) fill band = fill.between(upper, lower, color: color.withAlpha(color.gray, 24)) ``` A `plot` declaration creates an output and sets its default look. Nothing is drawn yet. Without an `on:` argument a plot goes on the price pane, which is right here because all three lines are prices. `fill.between` shades the area between two plots. The fill follows its plots automatically. Where either plot has no value, the fill has a gap too. ## 5. Calculate on each update ```flowscope on chart.update { let sma = ta.sma(chart.close, length) let hma = ta.hma(chart.close, length) let ema = ta.ema(chart.close, length) let avg = kind == "SMA" ? sma : kind == "HMA" ? hma : ema let atr = chart.atr(length) // ... } ``` `on chart.update` runs every time the chart subscription changes. Over history that is once per bar. On the live bar it runs again for every change of the forming candle. All three averages are computed on every bar, then the ternary chain picks one. Every `ta.*` call keeps its own history, so it must run on every bar: a call inside a branch that only runs sometimes would skip bars. Compute unconditionally, use conditionally. Notice how the long expression breaks after `:`. A line may end in an operator, and the expression continues on the next line. `chart.atr(length)` measures volatility from high, low and close, so it is a method of the subscription rather than a function of one field. > [!NOTE] > `ta.sma`, `ta.ema` and the other indicators return `null` until they have enough bars. The plot leaves those first points empty. You do not have to handle warm-up yourself. ## 6. Colour by slope ```flowscope let rising = ta.rising(avg, 1) ?? false mid.plot(avg, color: rising ? upColor : downColor) ``` `ta.rising(avg, 1)` is `true` when the average is higher than one bar ago. It can be `null` during warm-up, and a condition needs a definite `bool`. `?? false` supplies the fallback. The `color:` argument of `plot` overrides the declared colour for this one point. The line therefore switches colour bar by bar. ## 7. Plot the band only when it exists ```flowscope if avg != null && atr != null { upper.plot(avg + atr * bandMult) lower.plot(avg - atr * bandMult) } ``` `avg` and `atr` are local values of type `float?`. After the `!= null` check, both are treated as plain `float` inside the block, so the arithmetic is safe. When the check fails, neither band plot is fed for that bar and the chart shows a gap. Arithmetic with `null` gives `null` anyway, so `upper.plot(avg + atr * bandMult)` without the check would also produce a gap. The explicit check makes the intent obvious to a reader. ## 8. Try it 1. Open the script editor (**Script editor** in the bottom bar), paste the finished script and save it. 2. Press **Add to chart**, for example on ETHUSDT on Binance futures at 15m. 3. Click the gear on the script's legend row, switch the average to HMA and widen the band. 4. Switch the chart to an aggregated market. The indicator re-runs on the merged candles of all venues. ## Where to go next - Add a second subscription, such as open interest or market statistics, with [data and update events](/docs/scripting/guides/data-and-events). - Move the band calculation into a function with [functions](/docs/scripting/guides/functions). - Learn how the live bar can change a signal in [repainting](/docs/scripting/guides/repainting). - Turn the band into a backtest with [strategies](/docs/scripting/strategies). Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/getting-started/first-indicator --- # Coming from Pine Script > Map what you know from TradingView Pine Script to Flowscope Script, with side-by-side snippets. If you write Pine Script on TradingView, most of your ideas carry over. The indicators in `ta.*` behave the way you expect, plots and inputs look familiar, and strategies follow the same broker model. What changes is the structure. A Flowscope script declares its data explicitly and reacts to events, instead of running one body top to bottom on every bar. This page maps Pine concepts to Flowscope Script. It assumes you know Pine; everything else is explained in the [guides](/docs/scripting/guides). ## The big differences 1. **No implicit series.** There is no global `close`. You subscribe to candles with `data chart = subscribe(data.ohlcv)` and read `chart.close`. 2. **Handlers instead of one body.** Code runs in `on chart.update { }`, `on chart.close { }` and similar handlers. A script can have several, one per subscription and event. 3. **Typed and null-aware.** Market values are `float?`. There is no `na`; you write `null` and use `??` for fallbacks. The compiler tells you where a value might be missing. 4. **Persistence is declared.** Values that survive between bars are `state` declarations at the top level, not `var` inside the body. 5. **Other timeframes and markets are subscriptions.** `request.security` becomes another `data` line. Volume delta, open interest, liquidations, funding and trades are subscriptions too. ## Concept map | Pine Script | Flowscope Script | Notes | |---|---|---| | `//@version=6` + `indicator("Name")` | `script "Name"` | The header is always the first line. | | `strategy("Name", ...)` | `script "Name"` + `strategy(...)` | Same arguments in camelCase. See [strategy declaration](/docs/scripting/strategies/declaration). | | `close`, `high`, `volume` | `chart.close`, `chart.high`, `chart.volume` | After `data chart = subscribe(data.ohlcv)`. | | `close[1]` | `chart.close[1]` | History indexing works on subscription fields. | | `na`, `na(x)` | `null`, `x == null` | | | `nz(x, 0)` | `x ?? 0.0` | Parenthesise `??` when mixing with `&&` or `\|\|`. | | `var float level = na` | `state level: float? = null` | Top-level declaration. | | `varip` | `state` updated in `on x.update` | Live updates of the forming bar run the handler. | | `input.int(14, "Length")` | `input.int(14, title: "Length")` | Arguments after the default are named. | | `input.source(close)` | `input.select("close", options: [...])` | Pick the field yourself in the handler. | | `ta.ema(close, 20)` | `ta.ema(chart.close, 20)` | Same names for most indicators. | | `ta.atr(14)` | `chart.atr(14)` | OHLCV indicators take the subscription. | | `[m, s, h] = ta.macd(close, 12, 26, 9)` | `let m = ta.macd(chart.close)` then `m.macd`, `m.signal`, `m.hist` | Several outputs come back as one record; its fields are null during warm-up. Likewise `ta.bb` and `chart.kc()` (`upper`, `middle`, `lower`), `chart.dmi()` (`plus`, `minus`, `adx`), `chart.supertrend()` (`line`, `direction`). | | `request.security(sym, "240", close)` | `data h4 = subscribe(data.ohlcv, timeframe: 4h)` then `h4.close` | Confirmed value by default. | | `barstate.isconfirmed` | `on chart.close { }` | The handler only runs on confirmed bars. | | `barstate.isrealtime` | `chart.isRealtime` | | | `barstate.islast` | `chart.isLast` | True only at the live edge in update handlers. | | `bar_index` | `chart.index` | | | `time` | `chart.time` | | | `plot(x, color=c)` | `p.plot(x, color: c)` | `p` is declared once with `plot p = plot.line(...)`. | | `hline(50)` | `level.plot(50.0)` on a `plot.line` | Feed a constant. | | `bgcolor(c)` | `bg.plot(1.0, color: c)` on a `plot.bg` | | | `fill(p1, p2)` | `fill f = fill.between(p1, p2, color: c)` | | | `indicator(overlay=false)` | `pane osc = pane(...)` and `on: osc` | Without `on:`, plots go on the price pane. | | `line.new`, `label.new`, `box.new` | `entities.linePool`, `labelPool`, `boxPool` | `pool.get(key).set(...)`: reuse the key to move an object. | | `table.new` | `table.new(columns: [...])` in a `window` | | | `alertcondition`, `alert()` | `alert a = alert(...)` and `a.trigger(...)` | Typed payloads, live only. | | `array.new_float(10)` | `float[](10)` | Fixed capacity. | | `map.new()` | `map(n)` | | | `type` / UDT | `type` | Methods go inside the type block. | | `method` | `fn` inside `type`, using `self` | | | `switch` | `match` | Ranges, guards and a `null` arm. | | `while`, `break`, `continue` | Not available | Use `for x in xs` or `for i in 0..n`. | | `x % 2 == 0` | `math.isEven(x)` | No `%` operator; `a - math.floor(a / b) * b` in general. | | `and`, `or`, `not` | `&&`, `\|\|`, `!` | | | `str.tostring(x)` | `string(x)` or `str.format("{0:.2}", x)` | | ## A moving average, side by side Pine Script: ```text //@version=6 indicator("EMA", overlay=true) len = input.int(21, "Length") plot(ta.ema(close, len), color=color.orange) ``` Flowscope Script: ```flowscope script "EMA" input len = input.int(21, title: "Length", min: 1) data chart = subscribe(data.ohlcv) plot ema = plot.line(title: "EMA", color: color.orange) on chart.update { ema.plot(ta.ema(chart.close, len)) } ``` The calculation is the same. The difference is that the data, the output and the trigger are each named once. ## Missing values instead of `na` In Pine, `na` silently flows through arithmetic, and `nz` replaces it. In Flowscope Script the type system tracks it. A value that might be missing has a `?` type, such as `float?`. ```text // Pine range = high - low pct = nz(range / close * 100, 0) ``` ```flowscope let range = chart.high - chart.low let close = chart.close let pct = close != null && close > 0.0 ? (range ?? 0.0) / close * 100.0 : 0.0 ``` Arithmetic with `null` gives `null`, just like `na`. A comparison with `null` is `false`. An `if` condition must be a definite `bool`, so you write `?? false` where a value could be missing. The [missing values](/docs/scripting/guides/missing-values) guide has the full rules. ## `var` becomes `state` ```text // Pine var float sessionHigh = na if ta.change(time("D")) sessionHigh := high sessionHigh := math.max(sessionHigh, high) ``` ```flowscope script "Session high" data ( chart = subscribe(data.ohlcv) daily = subscribe(data.ohlcv, timeframe: 1D) ) state sessionHigh: float? = null plot line = plot.line(title: "Session high", style: linestyle.step) on daily.open { sessionHigh = null } on chart.update { let h = chart.high if h != null { sessionHigh = math.max(sessionHigh ?? h, h) } line.plot(sessionHigh) } ``` There is no "detect a new day" trick. You subscribe to daily bars and reset in `on daily.open`. ## `request.security` becomes a subscription ```text // Pine htfClose = request.security(syminfo.tickerid, "240", close[1], lookahead=barmerge.lookahead_on) ``` ```flowscope data h4 = subscribe(data.ohlcv, timeframe: 4h) on chart.update { let htfClose = h4.close // ... } ``` Inside a handler of another subscription, `h4.close` is always the last **confirmed** four-hour close. That is the non-repainting behaviour you would build in Pine with `close[1]` and lookahead. If you want the developing bar, read `h4.forming.close` explicitly. See [multiple timeframes](/docs/scripting/guides/multiple-timeframes) and [repainting](/docs/scripting/guides/repainting). The same mechanism reaches other data, such as liquidations and funding: ```flowscope data ( chart = subscribe(data.ohlcv) stats = subscribe(data.stat) oi = subscribe(data.oi) ) ``` On a chart, subscriptions serve the chart's own market at the chart's interval or whole multiples of it. ## `barstate.isconfirmed` becomes a close handler ```text // Pine if barstate.isconfirmed and ta.crossover(fast, slow) alert("Cross up") ``` ```flowscope script "Cross alert" input ( fastLen = input.int(9, title: "Fast", min: 1) slowLen = input.int(21, title: "Slow", min: 2) ) data chart = subscribe(data.ohlcv) alert crossUp = alert(title: "Cross up", onClose: true) on chart.close { let fast = ta.ema(chart.close, fastLen) let slow = ta.ema(chart.close, slowLen) if ta.crossover(fast, slow) ?? false { crossUp.trigger() } } ``` `on chart.close` runs once per confirmed bar, so the cross cannot appear and then vanish on the live bar. ## Strategies Strategy code reads almost the same. The settings move into a `strategy` declaration and orders go into a handler: ```text // Pine strategy("Cross", initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=10, commission_value=0.04) if ta.crossover(fast, slow) strategy.entry("L", strategy.long) ``` ```flowscope script "Cross" strategy( initialCapital: 10000.0, defaultQtyType: strategy.percentOfEquity, defaultQtyValue: 10.0, commissionValue: 0.04 ) data chart = subscribe(data.ohlcv) on chart.close { let fast = ta.ema(chart.close, 9) let slow = ta.ema(chart.close, 21) if ta.crossover(fast, slow) ?? false { strategy.entry("L", strategy.long) } if ta.crossunder(fast, slow) ?? false { strategy.entry("S", strategy.short) } } ``` Orders fill from the next bar's open, as in Pine's default. Perpetual funding is charged from historical rates unless you set `fundingCosts: false`. Read [strategies](/docs/scripting/strategies) for the full property list and the broker emulator. ## Things that do not exist | Pine feature | What to do instead | |---|---| | `while`, `break`, `continue` | Bounded `for` loops. Filter with `if` inside the loop. | | `%` | `math.isEven(x)`, or `a - math.floor(a / b) * b`. | | Semicolons, several statements on a line | One statement per line. | | `na()` function | `x == null`. | | `security` with lookahead on history | Not possible. History never sees the future. | | Implicit `close` source | Read the field you need from a subscription. | | `ta.*` inside a loop | Every `ta.*` call has its own state, so call it at handler level. | When the compiler rejects something, the message has a code. Look it up in [diagnostics](/docs/scripting/reference/diagnostics); every entry shows a failing script and the fix. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/getting-started/coming-from-pine --- # Guides > Task-oriented explanations of the ideas most Flowscope scripts rely on, from subscriptions and handlers to repainting. The getting-started pages show you the shape of a script. The guides go one level deeper: each one takes a single idea, explains how the language treats it, and walks through complete scripts you can paste into the editor and run against a live market. You do not have to read them in order. If you are new to Flowscope Script, start with data and update events, then missing values and repainting, because those three pages explain behaviour that every indicator depends on. Pick up the others when a script needs them. ```cards [Build and verify a script](/docs/scripting/guides/build-and-verify) Check data, warm-up, signal timing and runtime with a short, repeatable workflow. [Data and update events](/docs/scripting/guides/data-and-events) Subscribe to candles, flow, liquidations and trades, and decide which handler should react to each event. [Order book and liquidation maps](/docs/scripting/guides/heatmap-data) Read the heatmap's walls, the liquidity in a range, pulled and filled walls, and the liquidation model's clusters by leverage. [User-defined types](/docs/scripting/guides/user-defined-types) Group related values into your own records so state, function results and alert payloads stay readable. [Functions](/docs/scripting/guides/functions) Extract repeated calculations into typed functions, return several values at once and reuse stateful analysis safely. [Lambdas and reducers](/docs/scripting/guides/lambdas-and-reducers) Transform, filter and fold collections with short pure lambdas instead of hand-written loops. [Pattern matching](/docs/scripting/guides/pattern-matching) Replace chains of if and else with match arms over values, ranges, guards and null. [Colors and gradients](/docs/scripting/guides/colors-and-gradients) Build readable palettes, transparency, conditional colors and value-driven color scales. [Custom axes and charts](/docs/scripting/guides/custom-axes-and-charts) Draw bar charts, curves, tables and heatmaps in windows whose x-axis is an index or a category instead of time. [Inputs and visuals](/docs/scripting/guides/inputs-and-visuals) Give users a small, well-organised settings panel and outputs that stay legible on a busy chart. [Missing values](/docs/scripting/guides/missing-values) Understand how null flows through arithmetic, comparisons and plots, and when a fallback is honest. [Multiple timeframes](/docs/scripting/guides/multiple-timeframes) Combine subscriptions on different timeframes and venues without drawing levels one period late. [Repainting](/docs/scripting/guides/repainting) Choose deliberately between forming and confirmed values so signals mean what they promise. ``` ## How the guides are written Every guide follows the same conventions: - Code blocks that start with `script "..."` are complete scripts. They compile on their own and use the chart's market unless they say otherwise. - Shorter fragments omit the header and declarations that are not relevant to the point being made. - Examples lean on Flowscope's own data: aggregated markets (`exchange: "all"`), size-band flow from Bar Stats, liquidations, funding and the Hyperliquid position maps. They work just as well on a single venue. > [!TIP] > When you need an exact signature, a default value or the meaning of an error code, go to the [language reference](/docs/scripting/reference), the [built-in catalog](/docs/scripting/reference/catalog) or the [diagnostics list](/docs/scripting/reference/diagnostics). The guides explain why; the reference states what. ## Related pages - [Performance](/docs/scripting/performance) explains how to keep scripts responsive once they grow. - [Strategies](/docs/scripting/strategies) builds on the same handlers to run backtests and paper trading. - [Examples](/docs/scripting/examples) collects larger scripts that combine several guides at once. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides --- # Build and verify a script > A short path from an idea to a checked indicator: data coverage, warm-up, stable signals, runtime checks and common mistakes. Start with the smallest working signal. Add a dashboard, extra markets and alerts after the signal has been checked. This is also the recommended workflow for the AI assistant. ## Decide what the signal promises State the market and chart interval, the condition for long and short, and **when the condition becomes knowable**. Use these defaults unless the trader asks for something else: - Decisions on `on chart.close`, using confirmed higher-timeframe values. - Live previews in `on chart.update`, clearly named as developing. - Entries marked at detection time, never moved to an earlier pivot. - No invented substitute for missing market data. A missing filter blocks a signal; it does not become a neutral or passing filter via `?? 0.0`. A pivot with three right bars requires three later bars. It cannot be both confirmed and known on the pivot bar. A structural line may point back to that pivot, but an entry marker must show when the pivot was detected. For an earlier signal, change the rule to a causal condition such as a break of an already known level; explain that this is a different signal. See [repainting](/docs/scripting/guides/repainting). ## Check the data before writing the full script The chart host builds period subscriptions from loaded chart bars. Requested timeframes must be whole multiples of the chart interval: a 5s chart can supply 1m bars, but a 1m chart cannot supply 5s bars. Declaring a subscription does not fetch an independent, unlimited history for it. Count **source bars**, not chart bars. Fifty hourly candles require at least 50 hours of history (36,000 bars on a 5s chart), plus room for alignment and warm-up. A seeded EMA may return a number with much less history; a number alone does not prove that its warm-up is adequate. Loading more history can change its seed and early values. Other-market subscriptions on charts currently support `ohlcv`, `vd` and `cvd`. The quant lab does not supply other-market subscriptions, order-book snapshots or volume profiles. It rejects those requests rather than treating unavailable data as a successful study with no trades. Run those strategies on the chart and inspect the Strategy Tester instead. ## Use the shortest verification loop 1. Read `chart_context`. Select one relevant example and use `lookup_builtins` for uncertain signatures; names and arguments are case-sensitive. 2. Write a minimal, formatted script with named inputs and bounded collections. Read only the subscriptions the calculation needs. 3. Call `check_script`. It compiles, evaluates setup and checks timeframe/data requirements against the active chart without running handlers. Fix errors; inspect the reported number of confirmed bars and warm-up needs. 4. Call `apply_script` once the checks pass. Inspect runtime errors, computation time, actual subscription coverage, plotted values and marks. Empty output is a finding to investigate, not proof that no trading opportunities exist. 5. Use `look_at_chart` to check placement and readability. For a strategy, inspect `run_backtest`, its tested period, costs and trade count. 6. For a stability claim, step replay across a signal and across a higher-timeframe boundary. Record the detection bar. Advance several bars and compare it with the same period after replay/reload using the same inputs and source history. Compilation, successful execution, visible output and signal stability are four separate checks. Report which actually passed. A screenshot alone cannot verify that historical signals never changed. ## Common mistakes and direct fixes | Symptom | Check or fix | |---|---| | Compiles, but nothing appears | Subscription coverage, compatible timeframes, nulls, warm-up and signal conditions | | EMA or cross behaves differently after a filter change | Evaluate every `ta.*` call on every event of its intended clock, then filter its result; do not hide stateful calls inside `if` or short-circuit expressions | | Higher-timeframe EMA changes too often | Calculate it in `on higher.close` and store its result in `state`; do not feed repeated higher closes to a chart-clock EMA | | Entry suddenly appears several bars back | Pivot confirmation delay, negative `offset`, or an explicit past timestamp; separate pivot location from detection time | | Live arrow appears and vanishes | Forming data or an update handler; offer a separate confirmed signal | | Indicator trails the price | Distinguish candle-close waiting, pivot confirmation, indicator smoothing and computation time before changing the rule | | Slow chart or budget error | Remove per-bar scans, prefer `ta.*` or bounded rolling aggregates, draw status cards only under `isLast` | For runtime limits and browser scheduling, see [performance](/docs/scripting/performance). Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/build-and-verify --- # Data and update events > Subscribe to market data, react to open, update, close and trade events, and read earlier values. A subscription names a market-data series the script reads. For a chart indicator, start with the chart's candles: ```flowscope script "Chart data" data chart = subscribe(data.ohlcv) ``` `chart` is a name you choose, not a built-in global. It exposes candle fields such as `open`, `high`, `low`, `close` and `volume`. These can be missing, so their types are nullable (`float?`). ## Sources | Source | Record | What it holds | |---|---|---| | `data.ohlcv` | `OhlcvSub` | Candles: `open high low close volume buyVolume sellVolume buyCount sellCount trades`. | | `data.vd` | `VdSub` | Volume delta per bar as `open high low close`. | | `data.cvd` | `CvdSub` | Cumulative volume delta as `open high low close`. | | `data.oi` | `OiSub` | Open interest as `open high low close`. | | `data.stat` | `StatSub` | Market statistics: `sellLiq buyLiq markPrice fundingRate`. | | `data.trades` | `TradesSub` | Individual trades, delivered to an `on t.trade(tr)` handler. | | `data.book` | `BookSub` | Order-book snapshots, read from another subscription's handler. | | `data.volume` | `VolumeSub` | Volume-profile snapshots, read from another subscription's handler. | | `data.liqmap` | `LiqmapSub` | Liquidation and position level maps (the liquidation model, Hyperliquid's maps), read like books. A Flowscope extension. | ### What a chart provides On a chart, subscriptions serve the chart's own market: - **Candles, delta, CVD and open interest** at the chart's interval or a whole multiple of it (a 15m chart serves `timeframe: 1h`, not `5m`). - **Statistics** from the market's liquidations and funding: `sellLiq` and `buyLiq` are the value of liquidation orders in the bar (a sell liquidates a long), `fundingRate` the last settled rate (the current rate on the newest bar). `markPrice` is not available. Spot markets have no statistics. Bars before the session read the liquidations and open interest the market recorded (the hub's history), as the chart's Liquidations and Open Interest studies show them. - **Trades** from the trades the chart holds in memory. - **Order books** from the book the chart has recorded (what its liquidity heatmap shows): a period's snapshot is the heatmap's cell of that bar, the mean resting size at the bar's minute closes (on 1m bars the book at the minute's close). Before the recording starts, reads return `null`. - **Level maps** (`data.liqmap`) from the modeled liquidation levels the Liquidation Heatmap draws, or from Hyperliquid's position maps, each level averaged over the period by the time it stood. - **Volume profiles** from the footprints of the chart's bars: a snapshot sums the volume traded in a period by footprint row. While a script reads a profile, the chart loads footprints for the bars on screen; bars without one read `null`. Order books, volume profiles and level maps reach back **2,000 bars** from the newest bar (about 33 hours on 1m, 5.5 hours on 10s), up to 250 more: the start moves in steps of 250 bars, so a live run goes on from the last one instead of replaying everything with each bar. Older bars read `null`. A book for every bar of a long seconds chart would otherwise load and replay for minutes before the first value. Volume delta needs a volume split: the data hub's candles carry one, being built from recorded trades, and so does the live tape. ### Other exchanges and symbols Every source can come from other markets than the chart's. `exchange:` takes one exchange id, several joined with `markets.agg(...)`, or a list such as the value of an `input.exchanges` picker. Without `symbol:` each exchange's market of the chart's coin is read (`BTCUSDT` on Binance, `BTC-USDT-SWAP` on OKX, `BTC` on Hyperliquid, `BTC-USD` on Coinbase), so one script works on any chart: ```flowscope script "Spot CVD" setup spot = markets.agg("binance", "bybit", "okx", "coinbase", "kraken") data ( chart = subscribe(data.ohlcv) spotCvd = subscribe(data.cvd, exchange: spot) ) pane cvdPane = pane(title: "Spot CVD", height: 0.25) plot line = plot.line(title: "Spot CVD", on: cvdPane) on chart.update { line.plot(spotCvd.close) } ``` Several exchanges are combined into one market: | Source | From several exchanges | |---|---| | `data.ohlcv`, `data.vd`, `data.cvd` | Bought and sold volume added up; prices from the first exchange that traded in the bar. | | `data.oi` | Open interest added up. | | `data.stat` | Liquidations added up; `fundingRate` the mean of the perpetuals' rates. | | `data.trades` | Every exchange's prints, merged in time order. | | `data.book` | The books summed: each book shifted onto the first exchange's mid (USD, USDT and USDC quotes differ by their peg), then levels added up in price buckets of the widest exchange's bin. | | `data.volume` | The exchanges' footprints summed by price row. | The bars are the chart's bars: values are aligned to them by time, and a bar no exchange of the subscription traded in reads `null`. Order-book and volume-profile snapshots are those of the chart's periods, as for the chart's own market. The other markets open when the script starts on the chart and stay open while it runs; their history loads from the data hub, so the first values appear a moment later. A market the data hub does not capture reads `null`, and the script's data summary names it (`books: Kraken BTC/USD is not available (...); it reads null`). `check_script` accepts such subscriptions; whether a market is served shows once the script is on the chart. `markets.exchangeNames("spot")` and `markets.exchangeNames("futures")` list the exchange ids. ## Respond to updates Put calculations in a handler of the subscription that drives them: ```flowscope script "Candle range" data chart = subscribe(data.ohlcv) plot range = plot.histogram(title: "Candle range") on chart.update { let candleRange = (chart.high ?? 0.0) - (chart.low ?? 0.0) range.plot(candleRange) } ``` ## Several handlers Each subscription reacts to its own events, so different rates stay in separate, focused blocks. This script saves levels when an hourly candle closes and plots them as the chart advances: ```flowscope script "Previous hour high and low" data ( chart = subscribe(data.ohlcv) h1 = subscribe(data.ohlcv, timeframe: 1h) ) state ( prevHigh: float? = null prevLow: float? = null ) plot ( highPlot = plot.line(title: "Previous hour high", color: color.red, style: linestyle.step) lowPlot = plot.line(title: "Previous hour low", color: color.green, style: linestyle.step) ) on h1.close { prevHigh = h1.high prevLow = h1.low } on chart.update { highPlot.plot(prevHigh) lowPlot.plot(prevLow) } ``` - `on h1.close` runs when an hourly candle is confirmed and saves values that do not change afterwards. - `on chart.update` runs as the chart advances and draws the saved levels. `state` connects the handlers: what one writes, later calls of the other read. Period subscriptions support `open`, `update` and `close` handlers. A trades subscription uses a `trade` handler for each print. There is at most one handler per subscription and event. ## Statistics: liquidations and funding ```flowscope script "Liquidations" data ( chart = subscribe(data.ohlcv) stats = subscribe(data.stat) ) pane liqPane = pane(title: "Liquidations (USD)", height: 0.25) plot ( longs = plot.histogram(title: "Longs liquidated", color: color.red, on: liqPane) shorts = plot.histogram(title: "Shorts liquidated", color: color.green, on: liqPane) ) on stats.close { longs.plot(stats.sellLiq) shorts.plot(stats.buyLiq == null ? null : 0.0 - (stats.buyLiq ?? 0.0)) } ``` Funding as a line in basis points: ```flowscope script "Funding (bps)" data stats = subscribe(data.stat) pane fundingPane = pane(title: "Funding", height: 0.2) plot funding = plot.line(title: "Funding (bps)", color: color.amber, on: fundingPane) on stats.close { let rate = stats.fundingRate funding.plot(rate == null ? null : (rate ?? 0.0) * 10000.0) } ``` ## Trades A trades subscription runs its `trade` handler once for each print, oldest first. `minSize:` keeps only prints of at least that size. Each `tr` is a `Trade` with `time`, `price`, `size` and `isBuy`. ```flowscope script "Large buys" input minSize = input.float(5.0, title: "Smallest trade (base units)", min: 0.0) data ( chart = subscribe(data.ohlcv) tape = subscribe(data.trades, minSize: minSize) ) state bought = 0.0 pane buysPane = pane(title: "Large buy volume", height: 0.2) plot buys = plot.histogram(title: "Large buys", color: color.green, on: buysPane) on chart.open { bought = 0.0 } on tape.trade(tr) { if tr.isBuy { bought += tr.size } } on chart.update { buys.plot(bought) } ``` On a chart, trades reach back as far as the chart keeps them in memory, so older bars have no trades. ## Order books and volume profiles Book and profile subscriptions have no handlers of their own: read them from another subscription's handler. Each read gets the snapshot of the last period completed at that moment, so in `on chart.close` it describes the bar that just closed; `barsAgo:` reaches further back. ```flowscope script "Book imbalance and bar POC" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) profile = subscribe(data.volume) ) pane imbalancePane = pane(title: "Book imbalance within 1%", height: 0.2) plot ( imbalance = plot.histogram(title: "Imbalance", on: imbalancePane) poc = plot.marker(title: "Bar POC", shape: shape.diamond, color: color.amber, size: 4.0) ) on chart.close { // (bids - asks) / (bids + asks) by value, from -1 to 1. let ratio = book.imbalance(1.0) imbalance.plot(ratio, color: (ratio ?? 0.0) >= 0.0 ? color.green : color.red) let level = profile.poc() if level != null { poc.plot(level.price) } } ``` `book.imbalance` and `book.depth` count the whole book within the price band. `levels:` counts only the nearest levels of each side instead, the way a feed limited to N levels per side sees the book: `book.imbalance(2.5, levels: 250)` reads at most 250 price buckets (`book.binSize()` each) below the best bid and above the best ask. `profile.profile(n)` merges the last `n` periods into one profile with the same queries (`poc`, `valueArea`, `buckets`, `summary`). Walls, the liquidity in a price range, how a wall changed between periods and the liquidation map's clusters have their own queries on the heatmap's cells; see [order book and liquidation maps](/docs/scripting/guides/heatmap-data). ## Read earlier values Brackets read history on period fields: ```flowscope script "Previous close" data chart = subscribe(data.ohlcv) plot previous = plot.line(title: "Previous close") on chart.update { previous.plot(chart.close[1]) } ``` Earlier values can be `null`, especially near the start of the chart. Keep that absence where it matters instead of inventing a value. For live candles and close-confirmed signals, continue with [repainting and confirmed values](/docs/scripting/guides/repainting). Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/data-and-events --- # Order book and liquidation maps > Read the liquidity heatmap's walls, the liquidity in a price range and what became of a wall, and the liquidation model's clusters by leverage, from a script. A chart can show two heatmaps that look alike and mean very different things. - The **liquidity heatmap** is the order book: real orders resting at each price, recorded once a minute. A wall is liquidity someone placed, but it can be pulled before price gets there (spoofing), or eaten through. - The **liquidation heatmap** is a **model**: from open interest and taker flow it estimates where positions were opened, at which leverage, and so where they would be liquidated. Dense clusters act as magnets: price is drawn to them, and when it arrives the forced orders fire. On Hyperliquid the maps are built from public positions and orders instead, closer to real data. Scripts read both: `data.book` gives each bar's book as the liquidity heatmap's cells hold it, `data.liqmap` (a Flowscope extension) the level maps. Both are snapshot sources: they have no handlers of their own and are read from another subscription's handler, one period per bar. ## The book as the heatmap shows it The heatmap draws one cell per bar and price *bin* (about 0.012 % of the price on a round step: $10 on BTC). A cell holds the mean size its bin rested at the bar's minute closes; on 1m bars that is the book at the minute's close, and an order that rested two minutes of five counts two fifths of its size. A `data.book` period is exactly that bar's column: | Read | What it is | |---|---| | `book.binSize()` | The recorded price bucket of the levels (`bids()`, `asks()`, `depth()`). | | `book.cellSize()` | The heatmap's bin: the cells `sizeAt`, `wallAbove`, `wallBelow` and `change` use. | | `book.sizeAt(price)` | What rests in the cell of `price`: the wall's size there. | | `book.liquidity(from, to)` | Bids and asks resting from `from` to `to`. | | `book.wallAbove(pct)` / `book.wallBelow(pct)` | The largest cell within `pct` percent above or below the mid, or with `nearest: true` the nearest one of at least `minSize`. | | `book.change(price, bars: 1)` | The cell of `price` now against `bars` periods before. | Sizes are in base units by default; `unit: bookunit.quote` gives the notional. MMT takes the latest snapshot of each period instead of the mean; on one-minute bars both are the same. ```flowscope script "Nearest walls" input ( pct = input.float(0.5, title: "Search range (%)", min: 0.05, max: 5.0) minUsd = input.float(1000000.0, title: "Smallest wall (USD)", min: 0.0) ) data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) plot ( bidWall = plot.line(title: "Bid wall", color: color.green, style: linestyle.step) askWall = plot.line(title: "Ask wall", color: color.red, style: linestyle.step) ) on chart.close { let below = book.wallBelow(pct, minSize: minUsd, nearest: true, unit: bookunit.quote) let above = book.wallAbove(pct, minSize: minUsd, nearest: true, unit: bookunit.quote) bidWall.plot(below == null ? null : below.price + below.binSize / 2.0) askWall.plot(above == null ? null : above.price + above.binSize / 2.0) } ``` A found cell is a `PriceBin`: its lower edge `price`, its `top`, `size`, `value` (notional) and `distancePct` from the reference (the mid by default, `ref:` to choose). ## Pulled or filled A wall that is gone in the next bar either traded away or was cancelled. The book alone cannot tell; the volume traded at the wall's prices can. Compare the cell's size before and after with the footprint of the bar (`data.volume`): ```flowscope script "Pulled or filled walls" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) vol = subscribe(data.volume) ) plot ( pulled = plot.marker(title: "Pulled", shape: shape.cross, color: color.gray, size: 6.0) filled = plot.marker(title: "Filled", shape: shape.square, color: color.amber, size: 6.0) ) on chart.close { // The largest bid wall within 0.5 % on the bar before. let wall = book.wallBelow(0.5, barsAgo: 1) if wall == null { return } let c = book.change(wall.price + wall.binSize / 2.0) if c == null || c.before <= 0.0 || c.change > 0.0 - c.before / 2.0 { return } // Lost at least half: what traded at its prices in this bar? let s = vol.summary(from: wall.price - wall.binSize * 0.001, to: wall.top - wall.binSize * 0.001) let traded = s == null ? 0.0 : s.total if traded >= (0.0 - c.change) / 2.0 { filled.plot(wall.price) } else { pulled.plot(wall.price) } } ``` The book is sampled once a minute: a wall placed and pulled within a minute is never seen. The terminal's heatmap marks live pulls and fills as they happen; scripts work from the minute-sampled history, the same in backtests and live. ## Liquidation maps `subscribe(data.liqmap)` reads the liquidation model of the Liquidation Heatmap (Binance, Bybit and OKX futures and their aggregates). Each period holds the map's levels averaged over the period by the time they stood: long liquidations below the period's price (`below()`), short liquidations above (`above()`), each level as coins at its price, so its `value` is the notional. Every query reports notional in USD. | Read | What it is | |---|---| | `liq.densityBelow(pct)` / `liq.densityAbove(pct)` | The notional within `pct` percent below or above the price. | | `liq.density(outerPct, innerPct:)` | Both sides of a band at once, as a `LiqDensity`. | | `liq.clusterBelow(pct, minSize:)` / `liq.clusterAbove(pct, minSize:)` | The nearest cell of at least `minSize` within `pct` percent (`nearest: false`: the largest). | | `liq.sizeAt(price)` | The notional in the cell of `price`. | | `liq.change(price, bars: 1)` | A cell now against `bars` periods before: was the cluster swept? | | `liq.price()` | The market's close at the end of the period, the default reference. | ```flowscope script "Liquidation magnets" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liqmap) ) pane sidePane = pane(title: "Liquidations within 2 % (USD)", height: 0.2) plot ( below = plot.histogram(title: "Longs below", color: color.red, on: sidePane) above = plot.histogram(title: "Shorts above", color: color.green, on: sidePane) magnetDown = plot.line(title: "Cluster below", color: color.orange, style: linestyle.dotted) magnetUp = plot.line(title: "Cluster above", color: color.cyan, style: linestyle.dotted) ) on chart.close { below.plot(0.0 - (liq.densityBelow(2.0) ?? 0.0)) above.plot(liq.densityAbove(2.0)) let down = liq.clusterBelow(2.0, minSize: 2000000.0) let up = liq.clusterAbove(2.0, minSize: 2000000.0) magnetDown.plot(down == null ? null : down.price) magnetUp.plot(up == null ? null : up.top) } ``` ### Leverage tiers The model spreads every position it estimates over 5x, 10x, 25x, 50x, 75x and 100x (weights 10, 25, 30, 15, 8 and 12 %). High tiers sit close to the entry (100x about 0.6 % away), low tiers far from it (5x about 20 %). `minLeverage:` and `maxLeverage:` keep only the tiers between them: ```flowscope data highLev = subscribe(data.liqmap, minLeverage: 50.0) ``` The recorded levels (the session's and the hub's history) carry no split by leverage. The split of each level comes from the same model run again over the subscription's bars, open interest and taker volume, up to the period read; a level that run does not hold counts by the tiers' weights. The filtered map is therefore as exact as the bars: finer bars, a closer split. ### Hyperliquid's maps `map:` picks one of Hyperliquid's maps of the chart's coin, built from the public positions and trigger orders of the accounts Flowscope tracks: `liqmap.hlLiquidations` (liquidation prices), `liqmap.hlStops` (stop losses: long stops below, short stops above) and `liqmap.hlTakeProfits` (take profits: short ones below, long ones above). How complete they are depends on how much of the open interest the tracked accounts hold. ```flowscope data stops = subscribe(data.liqmap, map: liqmap.hlStops) ``` ## Real liquidations and open interest The maps say where liquidations *would* fire; `data.stat` says how much did, and `data.oi` whether positions closed. A sweep into a cluster that is real shows both: `stats.sellLiq` (longs liquidated) spiking and open interest falling in the same bar. On a chart the bars before the session read what the market recorded of both (the hub's history), as the Liquidations and Open Interest studies show them. ```flowscope script "Flush confirmation" data ( chart = subscribe(data.ohlcv) stats = subscribe(data.stat) oi = subscribe(data.oi) ) plot flush = plot.marker(title: "Long flush", shape: shape.up, color: color.green, size: 6.0) on chart.close { let longs = stats.sellLiq ?? 0.0 let avg = ta.sma(longs, 60) let open = oi.open let close = oi.close if avg == null || open == null || close == null || open <= 0.0 { return } if longs >= 3.0 * avg && longs >= 100000.0 && close < open * 0.999 { flush.plot(chart.low) } } ``` ## History Books, profiles and level maps reach back 2,000 bars from the newest bar (up to 250 more: the start moves in steps of 250 bars), and only as far as they were recorded: the session's own recording and the hours the data hub holds. Bars without a recording read `null`, never an invented book. Reads in `on chart.close` use the bar that just closed; on the forming bar the latest is the bar before. The same data gives the same values in a backtest and live. ## Related - [Liquidation Sweep Reversal](/docs/scripting/examples/liquidation-sweep-reversal) and [Wall Absorption](/docs/scripting/examples/wall-absorption), two strategies built on these reads. - [Data and update events](/docs/scripting/guides/data-and-events) for the sources and how other exchanges are read. - The [data family](/docs/scripting/reference/catalog/data) lists every method. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/heatmap-data --- # User-defined types > Give related values a name and behaviour with records, methods and typed collections. User-defined types give a name and behaviour to values that travel together: a price zone, a signal, a risk plan or the running state of a pattern. ## Declare a type `type` lists every field with its type: ```flowscope type Zone { low: float high: float touches: int } ``` Create a value by giving every field, one per line or on one line: `Zone { low: 1.0, high: 2.0, touches: 0 }`. Types are nominal: two types with the same fields are still different types. ## Methods Methods keep operations next to the data. The current value is `self`: ```flowscope script "Zone touches" data chart = subscribe(data.ohlcv) type Zone { low: float high: float touches: int fn width() -> float { return self.high - self.low } fn contains(price: float) -> bool { return price >= self.low && price <= self.high } fn recordTouch() { self.touches += 1 } } state zone = Zone { low: 0.0, high: 0.0, touches: 0 } state boxes = entities.boxPool(max: 1) on chart.close { let hi = ta.highest(chart.high, 50) let lo = ta.lowest(chart.low, 50) if zone.width() == 0.0 && hi != null && lo != null { // A zone at the bottom fifth of the 50-bar range. zone = Zone { low: lo, high: lo + (hi - lo) * 0.2, touches: 0 } } if zone.contains(chart.low ?? 0.0) { zone.recordTouch() } } on chart.update { let t = chart.time if chart.isLast && t != null && zone.width() > 0.0 { let label = str.format("{0} touches", zone.touches) boxes.get("zone").set(t - time.bars(50), zone.high, t, zone.low, color: #26A69A22, extend: extend.right, text: label) } } ``` Read-only methods return information; a method that assigns to `self` updates the state or local it was called on. ## Typed collections Types make collections easy to read: ```flowscope script "Recent signals" data chart = subscribe(data.ohlcv) type Signal { price: float strength: float bullish: bool } state recent = Signal[](50) pane countPane = pane(title: "Bullish of the last 50", height: 0.2) plot bullish = plot.histogram(title: "Bullish", color: color.green, on: countPane) on chart.close { if recent.len >= recent.capacity { recent.shift() } let open = chart.open ?? 0.0 let close = chart.close ?? 0.0 recent.push(Signal { price: close, strength: math.abs(close - open), bullish: close >= open }) let ups = recent.filter(|s| s.bullish) bullish.plot(float(ups.len)) } ``` ## Copying Assignment and function arguments copy a record; changing the copy leaves the original alone. Methods are the exception: `self` is the value the method was called on. Iterating an array also gives copies, so write a changed element back by index: ```flowscope script "Write back" data chart = subscribe(data.ohlcv) type Level { price: float hits: int } state levels = Level[](8) on chart.close { let close = chart.close ?? 0.0 if levels.len == 0 { levels.push(Level { price: close, hits: 0 }) } for i, level in levels { if math.abs(close - level.price) < context.tickSize * 10.0 { levels[i] = Level { price: level.price, hits: level.hits + 1 } } } } ``` ## When to introduce a type Create a type when it improves the model of the script: a result with several related parts, state with its own update rules, items stored in a collection, or logic that reads better as methods such as `contains` or `reset`. For a one-off pair of values, two `let` bindings are simpler. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/user-defined-types --- # Functions > Name a calculation, give it defaults, return several values, and reuse indicators inside helpers. Functions turn a calculation into a named operation: they remove repetition, keep handlers readable and give important rules one home. ## Declare and call a function Parameters and return values have explicit types: ```flowscope script "Candle strength" data chart = subscribe(data.ohlcv) fn candleStrength(open: float, high: float, low: float, close: float) -> float { let range = high - low return range > 0.0 ? math.abs(close - open) / range : 0.0 } pane strengthPane = pane(title: "Candle strength", height: 0.25) plot strength = plot.line(title: "Strength", on: strengthPane) on chart.update { strength.plot(candleStrength(chart.open ?? 0.0, chart.high ?? 0.0, chart.low ?? 0.0, chart.close ?? 0.0)) } ``` Functions are declared at the top level and may be called before they appear in the file. Candle strength is a 0–1 ratio, so it gets its own pane with `on:`. ## Defaults and named arguments Optional parameters follow required ones; defaults are literals. Named arguments make calls easy to read: ```flowscope script "Normalised move" data chart = subscribe(data.ohlcv) fn normalizedMove(value: float, scale: float = 1.0) -> float { return scale > 0.0 ? value / scale : 0.0 } pane movePane = pane(title: "Move in ATRs", height: 0.25) plot move = plot.line(title: "Move", on: movePane) on chart.update { let raw = (chart.close ?? 0.0) - (chart.open ?? 0.0) move.plot(normalizedMove(raw, scale: chart.atr(14) ?? 1.0)) } ``` ## Return several values A function can return a few related values without a type, and the caller takes them apart at once. `_` ignores a value: ```flowscope script "Candle parts" data chart = subscribe(data.ohlcv) fn candleParts(open: float, high: float, low: float, close: float) -> (float, float, float) { let body = math.abs(close - open) let upper = high - math.max(open, close) let lower = math.min(open, close) - low return body, upper, lower } pane partsPane = pane(title: "Wicks", height: 0.25) plot ( upperWick = plot.histogram(title: "Upper wick", color: color.red, on: partsPane) lowerWick = plot.histogram(title: "Lower wick", color: color.green, on: partsPane) ) on chart.close { let _, upper, lower = candleParts(chart.open ?? 0.0, chart.high ?? 0.0, chart.low ?? 0.0, chart.close ?? 0.0) upperWick.plot(upper) lowerWick.plot(0.0 - lower) } ``` Use a [user-defined type](/docs/scripting/guides/user-defined-types) when the result has an identity or methods of its own. ## Indicators inside helpers `ta.*` calls can live in functions. Each place the function is called keeps its own history, so the two calls below stay separate: ```flowscope script "Rate of change" data chart = subscribe(data.ohlcv) fn rateOfChange(value: float?, length: int) -> float? { return ta.roc(value, length) } pane rocPane = pane(title: "Rate of change (%)", height: 0.25) plot ( priceRoc = plot.line(title: "Price", color: color.blue, on: rocPane) volumeRoc = plot.line(title: "Volume", color: color.orange, on: rocPane) ) on chart.close { priceRoc.plot(rateOfChange(chart.close, 12)) volumeRoc.plot(rateOfChange(chart.volume, 12)) } ``` Calls that keep history cannot run inside loops or lambdas; call them from a handler or a helper. ## Early returns A bare `return` ends a function without a value early. A function with a return type must return a value on every path. ```flowscope script "Breakout marker" data chart = subscribe(data.ohlcv) plot breakout = plot.marker(title: "Breakout", shape: shape.up, color: color.green) fn markBreakout(price: float, level: float?) { if level == null { return } if price > (level ?? price) { breakout.plot(price) } } on chart.close { // The highest high of the 20 bars before this one. markBreakout(chart.close ?? 0.0, ta.highest(chart.high[1], 20)) } ``` ## Keep functions focused A good function does one of three things: computes and returns a value, updates a clearly named piece of state, or draws one coherent result. When it needs many related parameters, they probably belong in a user-defined type. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/functions --- # Lambdas and reducers > Transform collections with map, filter and reduce and short inline lambdas. Lambdas are short inline calculations for `map`, `filter` and `reduce`. Use them when an operation reads better as a collection transformation than as a loop. ## Map values `map` transforms every element and returns a new array: ```flowscope script "Average distance from the close" data chart = subscribe(data.ohlcv) state closes = rolling(100) pane distancePane = pane(title: "Average distance", height: 0.25) plot averageDistance = plot.line(title: "Average distance", on: distancePane) on chart.close { closes.push(chart.close) let close = chart.close ?? 0.0 let distances = closes.map(|price| math.abs((price ?? 0.0) - close)) averageDistance.plot(distances.avg()) } ``` The parameter between `|` is inferred from the collection, and the body is one expression, so there is no `return`. ## Filter values `filter` keeps the elements for which the expression is true and keeps their type. Null elements are passed in; `move > 0.0` is false for a missing value. ```flowscope script "Large moves" data chart = subscribe(data.ohlcv) input threshold = input.float(0.3, title: "Smallest move (%)", min: 0.0) state moves = rolling(50) pane countPane = pane(title: "Large up moves of the last 50", height: 0.2) plot count = plot.histogram(title: "Count", color: color.green, on: countPane) on chart.close { let open = chart.open let close = chart.close moves.push(open != null && close != null && open > 0.0 ? (close / open - 1.0) * 100.0 : null) let ups = moves.filter(|move| move > 0.0) let large = ups.filter(|move| move >= threshold) count.plot(float(large.len)) } ``` ## Reduce to one result `reduce` folds a collection into one value, starting from its first argument. For an empty collection the result is the start value. ```flowscope script "Move statistics" data chart = subscribe(data.ohlcv) state moves = rolling(30) pane statsPane = pane(title: "Moves over 30 bars", height: 0.25) plot ( total = plot.line(title: "Sum of absolute moves", on: statsPane) largest = plot.line(title: "Largest move", color: color.orange, on: statsPane) ) on chart.close { moves.push(ta.change(chart.close)) let absolute = moves.reduce(0.0, |acc, move| acc + math.abs(move ?? 0.0)) let biggest = moves.reduce(0.0, |best, move| math.max(best, math.abs(move ?? 0.0))) total.plot(absolute) largest.plot(biggest) } ``` ## One transformation per statement A higher-order call stands on its own: as the initialiser of a `let`, the right side of an assignment or a bare statement. Bind each stage before using it in a call, a conditional or `??`. Binding stages also makes them easy to inspect. Every stage creates a new array; for long pipelines over large collections a `for` loop does less work. ## What lambdas can read A lambda reads values from the surrounding scope, like `close` above. It only transforms data: plots, drawings and `ta.*` are rejected inside it, also through a helper function. Compute the collection result first, then draw. ## Collections, not series `map`, `filter` and `reduce` work on arrays and rolling windows. A market series such as `chart.close` is clocked history: use `chart.close[1]` and `ta.*` functions on it, or push the values you care about into a `rolling` first. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/lambdas-and-reducers --- # Pattern matching > Turn continuous values into named cases with match expressions and statements. `match` compares one value against ordered cases. It suits turning a continuous value into a named regime, choosing a style, handling missing data or acting on an enum. The first matching arm wins, so write the most specific cases first. ## Produce a value A match expression gives each case a result: ```flowscope script "Move regime" data chart = subscribe(data.ohlcv) fn describeMove(changePct: float?) -> string { return match changePct { null => "Waiting for data" >= 2.0 => "Strong rise" > 0.0 => "Rise" <= -2.0 => "Strong fall" < 0.0 => "Fall" _ => "Flat" } } state hud = entities.labelPool(max: 1, anchor: anchor.topLeft) on chart.update { let open = chart.open let close = chart.close let change: float? = open != null && close != null && open > 0.0 ? (close / open - 1.0) * 100.0 : null if chart.isLast { hud.get("move").set(12.0, 12.0, describeMove(change)) } } ``` Relational patterns compare the subject with the value after `<`, `<=`, `>` or `>=`. Because arms are checked top to bottom, `>= 2.0` comes before `> 0.0`. A match expression must cover every value; a final `_` is the simplest way. ## Choose a colour ```flowscope script "Delta colour" data chart = subscribe(data.ohlcv) pane deltaPane = pane(title: "Delta", height: 0.25) plot bars = plot.histogram(title: "Delta", on: deltaPane) on chart.close { let delta = chart.delta() let volume = chart.volume let share: float? = delta != null && volume != null && volume > 0.0 ? delta / volume : null let shade = match share { null => color.gray >= 0.3 => color.green <= -0.3 => color.red _ => color.yellow } bars.plot(delta, color: shade) } ``` ## Match statements Use the statement form to run actions. For one statement an arm needs no braces: ```flowscope script "Rising streak" data chart = subscribe(data.ohlcv) state risingBars = 0 pane streakPane = pane(title: "Rising streak", height: 0.2) plot streak = plot.histogram(title: "Bars", on: streakPane) on chart.close { let move = ta.change(chart.close) match move { null => risingBars = 0 > 0.0 => risingBars += 1 _ => risingBars = 0 } streak.plot(float(risingBars)) } ``` A statement match need not cover every value. ## Combine cases Separate alternatives with `|`: ```flowscope script "Session of the day" data chart = subscribe(data.ohlcv) fn sessionName(hour: int) -> string { return match hour { 0 | 1 | 2 | 3 | 4 | 5 => "Overnight" 6 | 7 | 8 | 9 | 10 | 11 => "Morning" 12 | 13 | 14 | 15 | 16 | 17 => "Afternoon" _ => "Evening" } } state hud = entities.labelPool(max: 1, anchor: anchor.bottomLeft) on chart.update { let t = chart.time if chart.isLast && t != null { hud.get("session").set(12.0, 12.0, sessionName(time.hour(t))) } } ``` Alternatives can mix exact and relational cases: `0.0 | < -3.0 => "Inactive"`. ## Handle missing values first For a nullable subject, put `null` in its own arm. Exact and relational patterns never match `null`; only `null` and `_` do. ## Match calculated values An exact pattern can be a variable or a calculation. The subject is computed once: ```flowscope script "Session open" input openHour = input.int(13, title: "Open hour (UTC)", min: 0, max: 23) data chart = subscribe(data.ohlcv) pane phasePane = pane(title: "Phase", height: 0.15) plot phase = plot.histogram(title: "Phase", on: phasePane) on chart.close { let t = chart.time if t != null { let value = match time.hour(t) { openHour => 2.0 > openHour => 1.0 _ => 0.0 } phase.plot(value) } } ``` Exact patterns use plain equality, so they suit integers, strings and enums. Compare prices with relational arms instead. ## What patterns do not do Patterns compare one subject. They do not bind names, destructure records or take `if` guards. Read fields into locals first, or compute an extra condition before the match. ## Match or if? - A match expression calculates one result from several cases of one value. - A match statement dispatches actions from one value. - `if` suits one or two unrelated conditions. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/pattern-matching --- # Colors and gradients > Named colours, hex and RGB colours, transparency, per-value colours, gradients and colour scales. Colour carries meaning: direction, intensity, primary versus context. Use it for that rather than decoration. ## Choose a colour Named colours include `color.red`, `color.green`, `color.blue`, `color.orange`, `color.teal`, `color.purple`, `color.amber`, `color.gray`, `color.white` and more; `color.up` and `color.down` follow the chart's candle colours. Hex literals give an exact value, `#RRGGBB` or `#RRGGBBAA` with alpha last; `color.rgb(r, g, b, a)` builds one from channels from 0 to 255. ```flowscope script "Colour sources" setup brand = #466BE5 setup soft = color.rgb(20, 184, 166, 72) data chart = subscribe(data.ohlcv) plot ( fast = plot.line(title: "Fast", color: brand, width: 2) slow = plot.line(title: "Slow", color: color.up) ) fill band = fill.between(fast, slow, color: soft) on chart.close { fast.plot(ta.ema(chart.close, 9)) slow.plot(ta.ema(chart.close, 21)) } ``` ## Let traders choose Expose the colours that carry meaning as inputs, and keep structural colours (grid fills, neutral context) inside the script. ## Colour individual values The declaration sets the default; a plot call can colour the current value: ```flowscope script "Momentum" input ( bullish = input.color(color.green, title: "Bullish") bearish = input.color(color.red, title: "Bearish", sameLine: true) ) data chart = subscribe(data.ohlcv) plot momentum = plot.histogram(title: "Momentum (%)", color: color.gray) on chart.close { let value = ta.roc(chart.close, 10) momentum.plot(value, color: (value ?? 0.0) >= 0.0 ? bullish : bearish) } ``` ## Transparency `color.withAlpha(c, a)` gives the same colour a different weight. Opaque colours suit lines and labels; low alpha suits fills and backgrounds, because the candles stay visible underneath. ## Gradients `color.gradient(value, bottom, top, start, end)` blends between two colours. Values outside the range take the end colours. ```flowscope script "Momentum gradient" data chart = subscribe(data.ohlcv) plot momentum = plot.histogram(title: "Momentum (%)") on chart.close { let score = math.clamp(ta.roc(chart.close, 10) ?? 0.0, -2.0, 2.0) momentum.plot(score, color: color.gradient(score, -2.0, 2.0, color.red, color.green)) } ``` ## Colour scales For more than two states, build a scale once in `setup` and sample it: ```flowscope script "Pressure scale" setup pressure = color.scale(at: [-2.0, 0.0, 2.0], colors: [color.red, color.gray, color.green]) data chart = subscribe(data.ohlcv) plot bars = plot.histogram(title: "Pressure") on chart.close { let value = ta.roc(chart.close, 10) ?? 0.0 bars.plot(value, color: pressure.sample(value)) } ``` ## Readability - Keep one colour for one meaning throughout a script. - Reserve vivid colours for actionable or unusual states. - Use alpha rather than a second unrelated colour for context. - Do not rely on red and green alone: a marker shape or the sign helps. The [colour reference](/docs/scripting/reference/catalog/color) lists every named colour and colour function. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/colors-and-gradients --- # Custom axes and charts > Windows with tables, heatmaps and charts whose x axis is a number, a category or your own time. Most indicators place values on the chart's time axis. A `window` holds panels with their own axes: charts keyed by numbers or names, tables and heatmaps. Use them for distributions, curves, venue comparisons and dashboards. Each window floats over the chart it belongs to; collapse it with its arrow, or hide the script to close it. ## A research window This script measures the average next-bar return for each RSI bucket and the average candle range per session: ```flowscope title="custom-axis-research.fs" script "Custom-axis research" data chart = subscribe(data.ohlcv) window research = window(title: "Market research") { response: chart.panel(axis: axis.index, title: "RSI bucket vs next-bar return"), sessions: chart.panel(axis: axis.category, title: "Average range by session"), } plot ( averageReturn = plot.line(title: "Average return %", color: color.teal, width: 2.5, on: research.response) latestReturn = plot.point(title: "Latest sample", color: color.white, size: 5.0, on: research.response) sessionRange = plot.histogram(title: "Average range %", color: color.orange, on: research.sessions) ) state ( returnSums = map(10) returnCounts = map(10) rangeSums = map(4) rangeCounts = map(4) previousClose: float? = null previousBucket: int? = null ) on chart.close { let close = chart.close ?? 0.0 let prior = previousClose let bucket = previousBucket if prior != null && prior != 0.0 && bucket != null { let sample = (close / prior - 1.0) * 100.0 let total = (returnSums.get(bucket) ?? 0.0) + sample let count = (returnCounts.get(bucket) ?? 0) + 1 returnSums.set(bucket, total) returnCounts.set(bucket, count) averageReturn.set(bucket, total / float(count)) latestReturn.set(bucket, sample, color: sample >= 0.0 ? color.green : color.red) } let t = chart.time ?? time.fromUnixSeconds(0) let hour = time.hour(t) let session = hour < 8 ? "Asia" : hour < 13 ? "Europe" : hour < 21 ? "US" : "Late" let high = chart.high ?? close let low = chart.low ?? close let rangePct = close != 0.0 ? (high - low) / close * 100.0 : 0.0 let rangeTotal = (rangeSums.get(session) ?? 0.0) + rangePct let rangeCount = (rangeCounts.get(session) ?? 0) + 1 rangeSums.set(session, rangeTotal) rangeCounts.set(session, rangeCount) sessionRange.set(session, rangeTotal / float(rangeCount)) let rsi = ta.rsi(chart.close, 14) ?? 50.0 previousBucket = int(math.floor(rsi / 10.0)) * 10 previousClose = close } ``` ## Choose the x axis `chart.panel(axis: ...)` creates a chart panel in a window: | Axis | x values | Use for | |---|---|---| | `axis.index` | Integers | Curves, buckets, ranks, levels | | `axis.category` | Strings | Venues, symbols, sessions, named groups | | `axis.time` | Times you choose | Synthetic series such as custom bars | Panes under the chart always follow the chart's time axis; a custom axis needs a window panel. ## set instead of plot Time-series plots get one value per bar with `plot`. Keyed charts keep one point per key with `set`; setting a key again updates it: ```flowscope averageReturn.set(40, 0.12) latestReturn.set(40, 0.18, color: color.green) sessionRange.set("US", 0.44) ``` Several plots on one panel share its axes, as the line and the points do above. Lines, points and histograms are drawn on chart panels. ## Tables A table panel shows rows by key; `row(key, cells...)` sets a whole row with one cell per column, `set(key, column, cell)` one cell, `rowBg(key, color)` a row's background: ```flowscope script "Market table" data ( chart = subscribe(data.ohlcv) stats = subscribe(data.stat) ) window dash = window(title: "Market") { table: table.new(columns: [table.col(title: "Metric"), table.col(title: "Value", align: align.right)]), } on chart.update { if chart.isLast { dash.table.row("close", "Close", str.format("{0:,.2}", chart.close ?? 0.0)) dash.table.row("volume", "Volume", str.format("{0:,.0}", chart.volume ?? 0.0)) let rate = stats.fundingRate dash.table.row("funding", "Funding", rate == null ? "–" : str.format("{0:.4%}", rate ?? 0.0)) } } ``` `table.cell(value, fg:, bg:, align:)` styles a single cell. ## Heatmaps `heatmap.new(x:, y:, capacity:)` shows cells by two keys, coloured from low to high (or along `palette:` colours between `min:` and `max:`). Set a cell with `panel.set(x, y, value)`. A full heatmap drops its oldest cell for a new one, so a heatmap with one column per bar scrolls with the chart. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/custom-axes-and-charts --- # Inputs and visuals > Expose clear settings, organise them, and choose the right plot for each output. Inputs are the settings traders see when they click the gear on a script's legend row. Give them clear labels and sensible ranges; defaults should produce a useful chart without any configuration. ```flowscope script "MA spread" input ( fastLength = input.int(12, title: "Fast length", min: 1, max: 200) slowLength = input.int(26, title: "Slow length", min: 2, max: 400) bullishColor = input.color(color.green, title: "Bullish") bearishColor = input.color(color.red, title: "Bearish", sameLine: true) ) data chart = subscribe(data.ohlcv) plot spread = plot.histogram(title: "MA spread") on chart.close { let fast = ta.ema(chart.close, fastLength) let slow = ta.ema(chart.close, slowLength) let value: float? = fast == null || slow == null ? null : (fast ?? 0.0) - (slow ?? 0.0) spread.plot(value, color: (value ?? 0.0) >= 0.0 ? bullishColor : bearishColor) } ``` A vertical histogram without `on:` gets its own pane automatically, named after the script. Other plots go on the price pane unless you declare a `pane` and pass `on:`. ## Input kinds | Constructor | Value | Notes | |---|---|---| | `input.int(default, min:, max:)` | `int` | A number field. | | `input.float(default, min:, max:)` | `float` | A number field. | | `input.bool(default)` | `bool` | A switch. | | `input.color(default)` | `color` | A colour picker. | | `input.select(default, options: [...])` | `string` | A dropdown of text options. | | `input.timeframe(default)` | `timeframe` | A timeframe. | | `input.exchange(default)`, `input.exchanges(default)` | `string`, `string[]` | Exchange pickers. | Every input takes `title:`, `description:` (a tooltip), `key:` (keeps saved settings when you rename the binding), `sameLine:` and `when:` (show the input only when a condition on other inputs holds). Changing an input re-runs the script over the loaded history. On a chart, the settings dialog edits every kind: a timeframe from a list (1m to 1W), one exchange from a dropdown and several with a switch per exchange. `filter:` limits the exchanges offered, `minSelected:` and `maxSelected:` how many may be picked; a saved choice outside them reads the default. ## Organise long settings Layout rows are written without a binding: ```flowscope script "Organised settings" input ( input.section(title: "Trend") trendLength = input.int(50, title: "Length", min: 2) showTrend = input.bool(true, title: "Show", sameLine: true) input.section(title: "Signals") signalStyle = input.select("Markers", title: "Style", options: ["Markers", "Background"]) ) data chart = subscribe(data.ohlcv) plot ( trend = plot.line(title: "Trend", color: color.blue) marks = plot.marker(title: "Above trend", shape: shape.circle, color: color.teal, size: 6.0) bg = plot.bg(title: "Above trend", color: #26A69A18) ) on chart.close { let avg = ta.sma(chart.close, trendLength) trend.plot(showTrend ? avg : null) let above = (chart.close ?? 0.0) > (avg ?? 0.0) if above && signalStyle == "Markers" { marks.plot(chart.high) } if above && signalStyle == "Background" { bg.plot() } } ``` `input.tab(title:)` puts the rows after it on a tab of its own (rows before the first tab go on the first; tabs that do not fit the dialog become a select). `input.group(title:, collapsible: true, collapsed: true)` heads the rows after it with a title that folds them (folded at first with `collapsed:`); every titled group and section has a **Reset** that returns its rows to the script's defaults. An exchanges picker lists the exchanges in Spot, Futures and Inverse columns; a saved pick keeps the exchanges the picker offers when one of them is gone. A section's `description:` is shown as text under its heading, which makes an About tab: ```text input.tab(title: "About") input.section(title: "Ratio", description: "(bids - asks) / (bids + asks) within the outer edge.") ``` For a visibility switch, keep the plot declared and feed it `null` when hidden, as `trend` does above, so the script's outputs stay the same. A level drawn as a line can put its price on the price scale: `axisLabel: true` on `entities.line.set` tags the line's right end in its colour, as an indicator's value (Key Levels' "Show Prices on Y-Axis"). ## Choose the plot | Plot | Use it for | |---|---| | `plot.line` | Averages, bands, oscillators. `style:` `linestyle.dotted`, `dashed` or `step`; `area: true` shades below. | | `plot.histogram` | Signed quantities such as delta, from zero, coloured per bar. | | `plot.candle`, `plot.bar` | OHLC of another series, such as a higher timeframe or a synthetic index. | | `plot.point` | Sparse values as dots. | | `plot.marker` | Events at a price, with a `shape:`. | | `plot.arrow` | Up or down events; the value's sign is the direction. | | `plot.label` | Short text at a price. | | `plot.bg` | Tint the bars where a condition holds. | | `plot.cells` | Rows of cells inside each bar: footprints, per-bar profiles, ladders. | | `fill.between` | Shade between two lines. | Free-form drawings (lines, boxes, labels anchored to the pane) are entity pools; see [repainting](/docs/scripting/guides/repainting#drawing-at-the-live-edge) for the pattern of drawing at the live edge. ## Cells inside a bar `plot.cells` draws rows of cells inside each bar: a price band, a width, colours and a number. It keeps them for far more bars than box entities can. This is a bid x ask footprint on rows of 25, with the chart's candles as a thin strip beside it: ```flowscope script "Footprint cells" data ( chart = subscribe(data.ohlcv) vol = subscribe(data.volume) ) plot footprint = plot.cells( title: "Bid x Ask", columns: 2, candles: cellcandles.side, format: cellformat.fixed, decimals: 2, ) on chart.update { let rows = vol.bucketsByStep(25.0) if rows != null { footprint.clear(time: rows.time) for row in rows { footprint.cell(row.from, row.to, weight: row.sell, value: row.sell, color: #e0443a80, column: 0, align: align.right, time: rows.time) footprint.cell(row.from, row.to, weight: row.buy, value: row.buy, color: #2fa66a80, column: 1, time: rows.time) } } } ``` - **Columns.** A bar has 1 to 4 columns side by side; `column:` picks one, `align:` the edge a cell's width grows from. - **Width.** `weight:` is compared as the plot's `scale:` says: with the largest weight on its own bar (`cellscale.bar`, the default), with the largest on the visible bars (`cellscale.view`), or taken as the width itself from 0 to 1 (`cellscale.unit`). Without a weight the cell fills its column. - **Numbers.** `value:` is written in the cell when it fits. `format:`, `decimals:`, `sign:` and `grouping:` choose `3.21K`, `3214.56`, `+3,215` or `15.3%`; a bar too narrow for that is written with fewer digits. - **Candles.** `candles:` leaves the chart's candles as they are (`cellcandles.overlay`), draws them as a thin strip at the left of each bar (`cellcandles.side`) or hides them (`cellcandles.hidden`). - **Steps.** The first `cell` or `clear` for a bar in a handler call discards what the bar held before, so a script redraws a bar by writing it again. Call `clear` where a bar may end up with no cells. `bucketsByStep` puts the rows on whole multiples of the step, so the same price is the same row on every bar. A bar holds up to 1024 cells per plot. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/inputs-and-visuals --- # Missing values > How null flows through arithmetic, comparisons, conditions and plots, and how to decide when a fallback is honest. Market data has holes. An indicator has no value until it has seen enough bars, a history read can reach past the first loaded bar, a spot market has no funding rate. The language represents all of these the same way: the value is `null`, and its type says so. ## Where null comes from Every type `T` has a nullable form `T?`. Market fields are nullable, so `chart.close` is a `float?`. | Source | Example | |---|---| | Indicator warm-up | `ta.sma(chart.close, 50)` is `null` for the first 49 bars. | | History before the first bar | `chart.close[10]` on the fifth bar. | | Data the market does not have | `stats.fundingRate` on a spot market. | | Data a chart does not serve | A subscription to another symbol on a chart. | | Collection reads | `xs[i]` out of range, `m.get(key)` for a missing key, `xs.pop()` on an empty array. | | Failed conversions | `int(x)` of a non-finite or out-of-range value. | ## The rules | Operation | Result with a null operand | |---|---| | Arithmetic `+ - * /` | `null` | | Ordering `< <= > >=` | `false` | | `x == null` / `x != null` | Whether `x` is absent / present | | `a == b` with a null side (even both) | `false`, and `a != b` is `true` | | `a ?? b` | `b` when `a` is null, else `a` | | Conditions of `if`, `?:`, `&&`, `\|\|`, `!` | Must be a non-null `bool` | | `p.plot(null)` | A gap at that bar | There is no `na()`, no `?.` and no postfix `!`. ## Narrowing After a check that proves a local is not null, it is treated as non-null for the rest of that block, including the right side of `&&`: ```flowscope script "Narrowing" data chart = subscribe(data.ohlcv) plot ret = plot.line(title: "Return (%)") on chart.close { let prior = chart.close[1] let close = chart.close if prior != null && close != null && prior > 0.0 { ret.plot((close / prior - 1.0) * 100.0) } } ``` Narrowing applies to locals and parameters, not to subscription fields, `state` or record fields: copy those into a local first, as above. ## Three honest ways to handle null ### Keep the gap If a value should not be drawn until it is valid, pass the nullable value on. The plot stays empty until the indicator has enough bars: ```flowscope script "Keep the gap" data chart = subscribe(data.ohlcv) plot average = plot.line(title: "SMA 20") on chart.update { average.plot(ta.sma(chart.close, 20)) } ``` ### Supply a fallback that means something Use `??` when the fallback is what you mean, such as zero volume for a bar without trades. Do not replace missing prices with zero to make an expression type-check: a zero price distorts the chart and implies data that never existed. ```flowscope script "Volume with fallback" data chart = subscribe(data.ohlcv) pane volPane = pane(title: "Volume", height: 0.2) plot vol = plot.histogram(title: "Volume", on: volPane) on chart.close { vol.plot(chart.volume ?? 0.0) } ``` ### Branch when absence changes the meaning ```flowscope script "Change from the previous close" data chart = subscribe(data.ohlcv) pane changePane = pane(title: "Change", height: 0.2) plot change = plot.histogram(title: "Change", on: changePane) on chart.close { let previous = chart.close[1] let delta: float? = previous == null ? null : (chart.close ?? previous) - (previous ?? 0.0) change.plot(delta) } ``` The first bar stays empty instead of reporting a change from zero. ## Typing a null A bare `null` takes its type from context: an annotation (`let x: float? = null`), a parameter type, or the other branch of a conditional. `c ? null : null` has no type and is an error. ## Collections Reads from collections are always nullable, and aggregates skip nulls: ```flowscope script "Last ten closes" data chart = subscribe(data.ohlcv) state closes = rolling(10) plot avg = plot.line(title: "Average of the last 10 closes") on chart.close { closes.push(chart.close) avg.plot(closes.len == 10 ? closes.avg() : null) } ``` `float[]` and `rolling` may hold `null`; other element types need a value. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/missing-values --- # Multiple timeframes > Read higher timeframes next to the chart's, choose confirmed or forming values, and build timeframes from the chart's. Declare another subscription when a calculation needs a different timeframe: ```flowscope script "Higher-timeframe EMA" input length = input.int(21, title: "4h EMA length", min: 2, max: 200) data ( chart = subscribe(data.ohlcv) h4 = subscribe(data.ohlcv, timeframe: 4h) ) plot ( confirmed = plot.line(title: "4h EMA", color: color.blue, width: 2, style: linestyle.step) developing = plot.line(title: "4h EMA (forming)", color: color.gray, style: linestyle.dashed) ) state ema: float? = null on h4.close { ema = ta.ema(h4.close, length) } on chart.update { confirmed.plot(ema) } on h4.update { // Inside its own update handler, h4 reads its forming candle. developing.plot(ta.ema(h4.close, length)) } ``` The confirmed EMA advances once per completed 4-hour candle and stays put afterwards; the forming one follows the developing candle. Label the difference so traders know which they see. See [repainting and confirmed values](/docs/scripting/guides/repainting). ## What the chart provides On a chart, other timeframes come from the chart's own bars: a subscription serves the chart's interval or a **whole multiple** of it, aggregated (open of the first bar, highest high, lowest low, close of the last, summed volumes). A 15m chart serves `1h` and `4h`; it cannot serve `5m`, because finer bars are not there. Change the chart to a finer interval for those. ## Reading other timeframes - In `h4`'s own `open` and `update` handlers, `h4.close` is the forming candle; in its `close` handler, the candle just completed. - In handlers of other subscriptions, `h4.close` is the last **confirmed** 4-hour close and `h4.forming.close` the developing one. The confirmed value needs no `[1]` offset. - `h4.close[1]` is the 4-hour close before the last confirmed one. ## Drawing at the live edge To show only the latest higher-timeframe levels, compute them on every bar in `on chart.update` and draw under `chart.isLast` there: ```flowscope script "Previous day high and low" data ( chart = subscribe(data.ohlcv) daily = subscribe(data.ohlcv, timeframe: 1D) ) state levels = entities.linePool(max: 2) on chart.update { let t = chart.time let start = daily.time let hi = daily.high let lo = daily.low if chart.isLast && t != null && start != null && hi != null && lo != null { levels.get("pdh").set(start, hi, t, hi, color: color.red, extend: extend.right, text: "PDH") levels.get("pdl").set(start, lo, t, lo, color: color.green, extend: extend.right, text: "PDL") } } ``` ## Timeframes from the chart's There is no `timeframe * n`; go through seconds with `time.tfSeconds` and `time.tfFromSeconds`. The result is a setup value, so it can drive a subscription: ```flowscope script "Four times the chart's timeframe" input mult = input.int(4, title: "Multiple", min: 1, max: 60) setup higherTf = time.tfFromSeconds(time.tfSeconds(context.timeframe) * mult) data ( chart = subscribe(data.ohlcv) higher = subscribe(data.ohlcv, timeframe: higherTf) ) pane ticksPane = pane(title: "Ticks from the higher close", height: 0.2) plot ticksAway = plot.histogram(title: "Ticks", on: ticksPane) on chart.update { let c = chart.close let h = higher.close if c != null && h != null { ticksAway.plot((c - h) / context.tickSize) } } ``` ## Where values are plotted Plots on the chart and its panes use the chart's time axis. A value written from a 4-hour handler lands on the chart bar containing that time, so a confirmed 4-hour value drawn from `on h4.close` appears once per 4-hour candle; drawing it from `on chart.update` repeats it on every chart bar, as in the first example. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/multiple-timeframes --- # Repainting and confirmed values > Choose deliberately between live and confirmed values, so signals do not appear and vanish on the forming bar. Repainting means a value visible now can change before it becomes final. That is not always a bug: a forming candle's high, close and volume, and indicators computed from them, naturally move until the candle closes. What matters is choosing deliberately between live and confirmed information. ## Confirmed and forming views Every period subscription has both views: ```flowscope script "Views" data ( chart = subscribe(data.ohlcv) h4 = subscribe(data.ohlcv, timeframe: 4h) ) plot ( confirmed = plot.line(title: "Last confirmed 4h close", style: linestyle.step) live = plot.line(title: "Forming 4h close", color: color.gray, style: linestyle.dashed) ) on chart.update { confirmed.plot(h4.close) live.plot(h4.forming.close) } ``` - `h4.close` is the last completed 4-hour close. It stays put until the next 4-hour candle confirms. - `h4.forming.close` is the developing 4-hour close and changes with every update. The same holds for open, high, low, volume and the other fields. ## Choose the event that matches the promise Use `on chart.update` for visuals that should follow the forming candle, and `on chart.close` for signals that promise candle-close confirmation: ```flowscope script "Close-confirmed cross" data chart = subscribe(data.ohlcv) plot ( average = plot.line(title: "EMA 20", color: color.blue) signal = plot.marker(title: "Cross up", shape: shape.up, color: color.green) ) on chart.close { let ema = ta.ema(chart.close, 20) average.plot(ema) if ta.crossover(chart.close, ema) { signal.plot(chart.low) } } ``` A close-confirmed signal appears one bar later than an intrabar one, but it cannot disappear because the candle moved back before closing. Strategies place orders in `on chart.close` for the same reason. ## Higher-timeframe indicators Higher timeframes are where accidental repainting is most common. A stepped 4-hour EMA on a lower-timeframe chart, built from confirmed closes only: ```flowscope script "Confirmed 4h EMA" data ( chart = subscribe(data.ohlcv) h4 = subscribe(data.ohlcv, timeframe: 4h) ) plot trend = plot.line(title: "4h EMA 21", color: color.blue, width: 2, style: linestyle.step) state ema: float? = null on h4.close { ema = ta.ema(h4.close, 21) } on chart.update { trend.plot(ema) } ``` The EMA advances once per confirmed 4-hour candle, so the unfinished candle cannot move it. If the developing value is useful, plot it separately and name it as such. ## Looking back never looks ahead History indexing always looks backwards: `chart.close[1]` is the previous close, `h4.high[1]` the 4-hour high before the last confirmed one. The confirmed view of a higher timeframe already points at the latest completed period, so there is nothing to shift to hide the forming one. ## Pivot location is not signal time `ta.pivotlow` and `ta.pivothigh` need the requested number of right-hand bars before a pivot is confirmed. With three right bars on a 5s chart, that means three later candles, about 15 seconds after the pivot candle closes. Moving an entry marker back to that pivot makes the history look earlier than the actual decision was. A negative plot offset or a drawing at `chart.time[3]` changes placement, not when the information became available. Keep entry markers at detection time. If you also draw the old pivot, label it as structure and keep it distinct from an entry. An earlier developing signal can disappear; show it separately rather than promising a confirmed pivot with no confirmation delay. ## What confirmation does not freeze Closed-bar logic prevents changes caused by the still-forming candle. It does not freeze source history: corrected or newly loaded bars, different warm-up history, edited inputs and changed markets can change a recalculation. In replay, a higher-timeframe candle is confirmed only when its own close is reached; a completed chart candle inside it is insufficient. To verify a script, compare detection bars while stepping replay forward and after reloading the same history with the same inputs. Record any differences. See [build and verify](/docs/scripting/guides/build-and-verify) for the full loop. ## Drawing at the live edge `chart.isLast` is true in `on chart.update` for the newest, live bar. It is false throughout history, during a [bar replay](/docs/app/replay), and always in `on chart.close`. Compute levels on every bar, then draw them under `chart.isLast` with a stable key, so each update moves the same drawing instead of adding one: ```flowscope script "Live 4h high" data ( chart = subscribe(data.ohlcv) h4 = subscribe(data.ohlcv, timeframe: 4h) ) state lines = entities.linePool(max: 1) on chart.update { let t = chart.time let h = h4.high let start = h4.time if chart.isLast && t != null && h != null && start != null { lines.get("h4-high").set(start, h, t, h, color: color.red, extend: extend.right, text: "4h high") } } ``` Use `chart.isRealtime` when a close handler needs to know whether the script is live. ## A checklist 1. Is each signal live or close-confirmed? 2. Does every higher-timeframe read use the confirmed or the forming view on purpose? 3. Are developing plots named as developing? 4. Do live drawings update one stable key instead of adding new ones? There is no rule that a script must never repaint: a live tape tool should update continuously, a candle-close alert usually should not. Make the choice visible in titles and names. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/guides/repainting --- # Strategies & backtesting > Turn a script into a strategy that places simulated orders and backtests on the chart's history, TradingView style. A **strategy** is a script that places orders as well as plotting. You decide *when* to enter and exit; Flowscope's broker emulator decides *how* those orders would have filled, keeps the books (position, equity, commission, funding) and reports the result in the [Strategy Tester](/docs/scripting/strategies/strategy-tester). The model follows TradingView's Pine `strategy()`: one `strategy(...)` declaration, order functions such as `strategy.entry` and `strategy.exit`, and read-only values such as `strategy.positionSize` and `strategy.equity`, with the same names in camelCase. Everything else is the language you already use for indicators. ## What changes when a script becomes a strategy | Indicator | Strategy | |---|---| | Plots, tables, drawings, alerts | Everything an indicator can do | | No notion of a position | A simulated account with position, equity and trades | | Output lives on the chart | Output also feeds the Strategy Tester | | — | Entry and exit marks with order ids on the chart | A script is a strategy as soon as it contains a `strategy(...)` declaration. There is at most one, usually right after the `script` header. ## A complete first strategy The script trades a fast/slow EMA crossover in both directions and protects every entry with a stop two ATRs away from the fill. ```flowscope title="ema-cross-atr-stop.fs" script "EMA cross with ATR stop" strategy( initialCapital: 10000.0, defaultQtyType: strategy.percentOfEquity, defaultQtyValue: 25.0, commissionType: strategy.commission.percent, commissionValue: 0.04, slippage: 1 ) input ( fastLen = input.int(12, title: "Fast EMA", min: 1, max: 200) slowLen = input.int(26, title: "Slow EMA", min: 2, max: 400) atrLen = input.int(14, title: "ATR length", min: 1, max: 100) stopAtr = input.float(2.0, title: "Stop distance (ATR)", min: 0.1) ) data chart = subscribe(data.ohlcv) plot ( fastLine = plot.line(title: "Fast EMA", color: color.blue, width: 2) slowLine = plot.line(title: "Slow EMA", color: color.orange, width: 2) ) on chart.close { let fast = ta.ema(chart.close, fastLen) let slow = ta.ema(chart.close, slowLen) let atr = chart.atr(atrLen) fastLine.plot(fast) slowLine.plot(slow) if atr != null { // The stop distance in ticks, fixed at signal time. let stopTicks = math.round(atr * stopAtr / context.tickSize) if ta.crossover(fast, slow) { strategy.entry("Long", strategy.long, comment: "EMA up") strategy.exit("Long SL", "Long", loss: stopTicks) } if ta.crossunder(fast, slow) { strategy.entry("Short", strategy.short, comment: "EMA down") strategy.exit("Short SL", "Short", loss: stopTicks) } } } ``` Open the script editor, paste it, and press **Add to chart**. The Strategy Tester opens at the bottom of the window with the backtest. ### What happens on each bar 1. `on chart.close` runs once per confirmed bar of the loaded history, oldest first. 2. On a bullish cross, `strategy.entry("Long", strategy.long)` queues a market order. If the strategy is short, the same order also closes the short: entries reverse. 3. `strategy.exit("Long SL", "Long", loss: ...)` attaches a stop to the `Long` entry, `loss` ticks from the actual fill price. 4. The emulator fills the market order at the **next bar's open** plus one tick of slippage, charges 0.04 % commission, and then watches the stop on every bar. > [!NOTE] > Orders placed at a bar's close fill at the next bar's open unless you declare > `processOrdersOnClose: true`. You decide after the close, so you can only trade > after it. See [Broker emulator](/docs/scripting/strategies/broker-emulator). ## History and live bars {#live} On a chart, a strategy runs over every loaded bar. When new bars arrive it runs again over the whole history (at most every 0.75 s while a bar is forming), so the Strategy Tester and the marks on the chart always show the backtest up to now. A forming bar is not traded: handlers in `on chart.close` see confirmed bars only. > [!IMPORTANT] > Strategies never send orders anywhere. There is no exchange connection in the > strategy engine. To act on signals yourself, declare an `alert` and trigger it > next to your order calls; alerts fire on live bars only. ## Where to put order calls Place order calls in `on chart.close`. Order functions run in handlers only; calling them from `setup` is a compile error. ## Reading the result The chart shows an arrow and label at each fill with the order id and size, a dotted line from entry to exit (green for a win, red for a loss), and a dashed line at the average entry price while a position is open. The Strategy Tester shows the P&L curve, the performance summary for all, long and short trades, and the list of trades. During a [bar replay](/docs/app/replay) the backtest stops at the replay bar and grows as the replay plays. ## Next steps ```cards [Declaration](/docs/scripting/strategies/declaration) Every strategy property: capital, sizing, commission, slippage, margin, funding. [Orders](/docs/scripting/strategies/orders) entry, order, exit, close and cancel: ids, pyramiding, reversals, brackets and trailing stops. [Position and trades](/docs/scripting/strategies/position-and-trades) Read the position, account values and trades from your script. [Broker emulator](/docs/scripting/strategies/broker-emulator) When and at what price orders fill, how costs and margin are charged. [Risk](/docs/scripting/strategies/risk) Risk-based sizing, drawdown stops, position caps and cooldowns, written in the script. [Strategy Tester](/docs/scripting/strategies/strategy-tester) The bottom panel: every metric defined, list of trades, CSV. ``` For complete worked strategies, see the [EMA and ATR strategy](/docs/scripting/examples/ema-atr-strategy) and the [CVD breakout strategy](/docs/scripting/examples/cvd-breakout-strategy) examples. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies --- # The strategy declaration > Every property of the strategy declaration, its type, default and effect on the simulated account. The `strategy(...)` declaration turns a script into a strategy and configures the simulated account and the broker emulator. It is a top-level declaration like `input` or `plot`, may appear once, and usually follows the `script` header. Properties are named arguments; leave out what you want at its default. ```flowscope script "Declaration demo" strategy( initialCapital: 25000.0, defaultQtyType: strategy.percentOfEquity, defaultQtyValue: 10.0, commissionType: strategy.commission.percent, commissionValue: 0.05, slippage: 2, pyramiding: 2, leverage: 5.0 ) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.sma(chart.close, 50)) { strategy.entry("Long", strategy.long) } } ``` `strategy()` with no arguments is valid and takes every default. ## Properties The names are TradingView's Pine `strategy()` arguments in camelCase. | Property | Type | Default | Meaning | |---|---|---|---| | `title` | `string` | the script name | Name in the Strategy Tester. | | `initialCapital` | `float` | `1000000.0` | Starting equity of the simulated account. | | `currency` | `string` | the quote asset | Account currency label. P&L is computed in the market's quote currency. | | `defaultQtyType` | `QtyType` | `strategy.fixed` | How orders without a `qty` are sized: `strategy.fixed`, `strategy.cash` or `strategy.percentOfEquity`. | | `defaultQtyValue` | `float` | `1.0` | Contracts, cash or percent of equity, per `defaultQtyType`. | | `pyramiding` | `int` | `0` | Entries allowed in the same direction; `0` and `1` both allow one. | | `commissionType` | `CommissionType` | `strategy.commission.percent` | `percent` of the order value, `cashPerContract` or `cashPerOrder`. | | `commissionValue` | `float` | `0.0` | Commission amount per `commissionType`. | | `slippage` | `int` | `0` | Ticks added against you to market and stop fills. | | `marginLong` | `float` | `100.0` | Margin for longs, percent of the position value. `100` is no leverage, `0` turns margin checks off. | | `marginShort` | `float` | `100.0` | Margin for shorts. | | `leverage` | `float` | — | Shorthand for both margins: `leverage: 5.0` sets them to 20 %. | | `processOrdersOnClose` | `bool` | `false` | Fill orders at the close of the bar that placed them instead of the next open. | | `closeEntriesRule` | `string` | `"fifo"` | `"fifo"`: exits close the oldest entry first. `"any"`: `strategy.close("id")` closes that entry's trades. | | `backtestFillLimitsAssumption` | `int` | `0` | Ticks price must trade through a limit before it fills. | | `fillOrdersOnStandardOhlc` | `bool` | `false` | Pine compatibility; charts already fill on standard bars. | | `useBarMagnifier` | `bool` | `false` | Fill along lower-timeframe bars when the host provides them. Charts do not yet, so this has no effect there. | | `calcOnOrderFills` | `bool` | `false` | Pine compatibility; not applied when a script runs on a chart. | | `calcOnEveryTick` | `bool` | `false` | Pine compatibility; strategies trade on confirmed bars. | | `fundingCosts` | `bool` | `true` | Charge or credit funding on perpetual markets at the venue's settlements. | | `riskFreeRate` | `float` | `2.0` | Annual rate in percent for the Sharpe and Sortino ratios. | Property values may reference inputs, so traders can change sizing or costs from the script settings without editing code: ```flowscope script "Sizing from inputs" input ( equityPct = input.float(10.0, title: "Position size (% equity)", min: 0.1, max: 100.0) feePct = input.float(0.04, title: "Taker fee (%)", min: 0.0) ) strategy( defaultQtyType: strategy.percentOfEquity, defaultQtyValue: equityPct, commissionType: strategy.commission.percent, commissionValue: feePct ) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(ta.ema(chart.close, 9), ta.ema(chart.close, 21)) { strategy.entry("Long", strategy.long) } } ``` ## Order size {#qty} An entry without `qty` takes its size from `defaultQtyType` and `defaultQtyValue`: | `defaultQtyType` | Size of an order | |---|---| | `strategy.fixed` | `defaultQtyValue` contracts (base units, e.g. 0.25 BTC). | | `strategy.cash` | `defaultQtyValue / price` contracts: a fixed notional. | | `strategy.percentOfEquity` | `equity × defaultQtyValue / 100 / price` contracts. | Sizes are rounded down to `context.stepSize` when the market reports one; charts do not know venue quantity steps yet, so sizes are not rounded there. An explicit `qty` (the third argument of `strategy.entry` and `strategy.order`) is a number of contracts; compute it in the script for other sizing rules, as in [Risk](/docs/scripting/strategies/risk). ## Commission {#commission} Commission is charged on every fill, entry and exit alike, and is part of each trade's profit. ```flowscope script "Maker and taker" strategy(commissionType: strategy.commission.cashPerOrder, commissionValue: 1.5) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.sma(chart.close, 20)) { strategy.entry("Long", strategy.long) } if ta.crossunder(chart.close, ta.sma(chart.close, 20)) { strategy.close("Long") } } ``` ## Margin and leverage {#margin} With margin below 100 %, the account can hold positions worth more than its equity. An entry the account cannot fund is skipped, never shrunk. When open losses eat into the margin, the emulator liquidates part of the position, as TradingView does; see [Broker emulator](/docs/scripting/strategies/broker-emulator#margin). ## Funding costs {#funding} On perpetual markets (Binance futures, Bybit, OKX, Hyperliquid, and aggregated markets that include them) positions pay or receive funding. With `fundingCosts: true` the backtest charges the funding rate the data hub recorded at each settlement the position is held through: `rate × position value`, longs paying a positive rate and shorts receiving it. It is booked at the first bar opening at or after the settlement, at that bar's open, and is part of the trade's profit and of the Strategy Tester's **Funding paid**. Aggregated markets use the open-interest weighted rate of their venues. Spot markets have no funding. ```flowscope script "Without funding" // Compare a run with and without funding to see what it costs the edge. strategy(fundingCosts: false, leverage: 2.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(ta.ema(chart.close, 20), ta.ema(chart.close, 50)) { strategy.entry("Long", strategy.long) } } ``` > [!NOTE] > A long held for days in a market paying 0.01 % every 8 hours gives up about 1 % > of its value per month. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies/declaration --- # Orders > Place, modify and cancel simulated orders with strategy.entry, order, exit, close and cancel. Order functions queue instructions for the broker emulator. They do not fill at once: the emulator works them from the next bar on (see [Broker emulator](/docs/scripting/strategies/broker-emulator)). They run in handlers only and return nothing. ## Overview | Function | Purpose | |---|---| | `strategy.entry(id, direction, qty?, limit:, stop:, ...)` | Open or add to a position; reverses an opposite one; respects `pyramiding`. | | `strategy.order(id, direction, qty?, limit:, stop:, ...)` | Raw buy or sell: no reversal, no pyramiding limit. | | `strategy.exit(id, fromEntry?, profit:, loss:, limit:, stop:, trailPoints:, trailOffset:, ...)` | Take-profit, stop-loss and trailing stop for an entry. | | `strategy.close(id, qty:, qtyPercent:, immediately:)` | Close an entry's trades at market. | | `strategy.closeAll(immediately:)` | Close the whole position at market. | | `strategy.cancel(id)` | Cancel the pending orders with this id. | | `strategy.cancelAll()` | Cancel every pending order. | `strategy.long` and `strategy.short` are the two directions. ## Order ids {#ids} Every order has a string id. Ids do three jobs: - **Identity.** Placing an order again with the id of a pending order *modifies* it instead of adding a second one. - **Linking.** `strategy.exit("TP", "Long")` and `strategy.close("Long")` refer to the entry with id `Long`. - **Reporting.** The id labels the marks on the chart and the Signal column of the list of trades, and is `entryId` / `exitId` in [trade records](/docs/scripting/strategies/position-and-trades#trades). Keep ids short and stable: an id built from a changing value creates a new order on every bar. ## Order lifetime {#lifetime} | Order | Lifetime | |---|---| | Market (no `limit`, no `stop`) | Fills at the next fill opportunity, then is gone. | | Limit or stop entry | Pending across bars until it fills or is cancelled. | | Exit | Pending while its entry is open; dropped when the position is flat and no entry is pending. | ## strategy.entry {#entry} ```text strategy.entry(id: string, direction: Direction, qty: float? = null, limit: float? = null, stop: float? = null, ocaName: string? = null, ocaType: OcaType? = null, comment: string? = null, alertMessage: string? = null) ``` - Without `limit` and `stop` it is a market order; with `limit` a limit order, with `stop` a stop order, with both a stop-limit order. - Without `qty` the size comes from `defaultQtyType` and `defaultQtyValue`. - An entry against the current position closes it and opens the new direction with `qty`: long 1 and `strategy.entry("S", strategy.short)` with size 1 sells 2. - Entries in the direction of the position are limited by `pyramiding`. - `alertMessage` raises an alert when the order fills, with Pine's placeholders filled in: `{{ticker}}`, `{{strategy.order.action}}` (`buy` or `sell`), `{{strategy.order.contracts}}`, `{{strategy.order.price}}`, `{{strategy.order.id}}` and `{{strategy.order.comment}}`. Fills already on the chart when the strategy is added are history and raise nothing. The other order calls take it the same way. ```flowscope script "Breakout entries" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 0.1) data chart = subscribe(data.ohlcv) on chart.close { let hi = ta.highest(chart.high, 20) let lo = ta.lowest(chart.low, 20) if hi != null && lo != null && strategy.positionSize == 0.0 { // Re-placed every bar: the stop orders follow the channel. strategy.entry("Up", strategy.long, stop: hi + context.tickSize) strategy.entry("Down", strategy.short, stop: lo - context.tickSize) } } ``` ## strategy.order {#order} `strategy.order` buys or sells exactly `qty` and ignores `pyramiding`. An order against the position reduces it by its size and only reverses it when it is larger. Use it to scale in and out by hand. ```flowscope script "Scale out" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) on chart.close { let ema = ta.ema(chart.close, 50) if ema != null { if strategy.positionSize == 0.0 && ta.crossover(chart.close, ema) { strategy.order("Buy", strategy.long, 1.0) } // Sell half when price stretches 2 % above the average. if strategy.positionSize > 0.5 && chart.close > ema * 1.02 { strategy.order("Trim", strategy.short, 0.5) } } } ``` ## strategy.exit {#exit} ```text strategy.exit(id: string, fromEntry: string? = null, qty: float? = null, qtyPercent: float? = null, profit: float? = null, limit: float? = null, loss: float? = null, stop: float? = null, trailPrice: float? = null, trailPoints: float? = null, trailOffset: float? = null, ocaName: string? = null, comment: string? = null, commentProfit: string? = null, commentLoss: string? = null, commentTrailing: string? = null, alertMessage: string? = null) ``` - `profit` and `loss` are distances in **ticks** from the entry's fill price; `limit` and `stop` are absolute prices. Give either form for each leg. - The trailing stop activates at `trailPrice`, or `trailPoints` ticks in profit, and then follows the best price `trailOffset` ticks behind. - Without `fromEntry` the exit applies to every open entry. - `qty` or `qtyPercent` (default 100) exits part of the entry. - The legs form one group: when one fills, the others are cancelled. ```flowscope script "Bracket" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(ta.ema(chart.close, 9), ta.ema(chart.close, 21)) { strategy.entry("Long", strategy.long) // 200 ticks target, 100 ticks stop. strategy.exit("TP/SL", "Long", profit: 200.0, loss: 100.0) } } ``` A trailing stop that starts once the trade is 150 ticks in profit and trails 50 ticks behind: ```flowscope script "Trailing stop" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.sma(chart.close, 100)) { strategy.entry("Long", strategy.long) strategy.exit("Trail", "Long", trailPoints: 150.0, trailOffset: 50.0, loss: 300.0) } } ``` ## strategy.close and strategy.closeAll {#close} `strategy.close("Long")` closes the trades of entry `Long` with a market order at the next fill opportunity; `qty` or `qtyPercent` closes part of them. With `immediately: true` the order fills at once, at the current close. `strategy.closeAll` closes the whole position. ```flowscope script "Time stop" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) state barsIn = 0 on chart.close { if strategy.positionSize != 0.0 { barsIn += 1 } else { barsIn = 0 } if strategy.positionSize == 0.0 && ta.crossover(chart.close, ta.ema(chart.close, 20)) { strategy.entry("Long", strategy.long) } // Out after 10 bars whatever happens. if barsIn >= 10 { strategy.closeAll(comment: "Time stop") } } ``` ## Cancelling {#cancel} `strategy.cancel("Up")` removes the pending orders with id `Up`; `strategy.cancelAll()` removes every pending order. Filled orders cannot be cancelled; close the position instead. ## One-cancels-all groups {#oca} Entries with the same `ocaName` form a group. With `ocaType: strategy.oca.cancel`, a fill cancels the other orders of the group; with `strategy.oca.reduce` it reduces them by the filled size. The breakout example above can use it so that only one of the two stop entries ever fills: ```flowscope script "OCA breakout" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) on chart.close { let hi = ta.highest(chart.high, 20) let lo = ta.lowest(chart.low, 20) if hi != null && lo != null && strategy.positionSize == 0.0 { strategy.entry("Up", strategy.long, stop: hi, ocaName: "break", ocaType: strategy.oca.cancel) strategy.entry("Down", strategy.short, stop: lo, ocaName: "break", ocaType: strategy.oca.cancel) } } ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies/orders --- # Position and trades > Read the open position, account values and trade records from a strategy script. A strategy can read its own state: the position, the account and every trade. The values come from the broker emulator and are current as of the bar the handler runs for. ## The open position {#position} | Property | Type | Meaning | |---|---|---| | `strategy.positionSize` | `float` | Contracts held: positive long, negative short, `0.0` flat. | | `strategy.positionAvgPrice` | `float?` | Average entry price, or null when flat. | | `strategy.positionEntryName` | `string?` | Id of the entry that opened the position, or null when flat. | | `strategy.openProfit` | `float` | Unrealised profit of the open trades at the current price, after their commission and funding. | | `strategy.marginLiquidationPrice` | `float?` | Price at which a margin call would start, or null. | Test the direction with the sign of the size: ```flowscope script "Position state" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) pane posPane = pane(title: "Position", height: 0.2) plot size = plot.histogram(title: "Size", on: posPane) on chart.close { let ema = ta.ema(chart.close, 30) if ema != null { if strategy.positionSize <= 0.0 && chart.close > ema { strategy.entry("Long", strategy.long) } if strategy.positionSize >= 0.0 && chart.close < ema { strategy.entry("Short", strategy.short) } } size.plot(strategy.positionSize) } ``` ## Account values {#account} | Property | Type | Meaning | |---|---|---| | `strategy.initialCapital` | `float` | The declared initial capital. | | `strategy.equity` | `float` | Initial capital + net profit + open profit. | | `strategy.netProfit` | `float` | Sum of the closed trades' profits. | | `strategy.grossProfit` | `float` | Sum of the winning trades' profits. | | `strategy.grossLoss` | `float` | Sum of the losing trades' losses, positive. | | `strategy.maxDrawdown` | `float` | Largest equity drawdown so far. | | `strategy.maxRunup` | `float` | Largest equity run-up so far. | | `strategy.openTrades` | `int` | Number of open trades. | | `strategy.closedTrades` | `int` | Number of closed trades. | | `strategy.winTrades` / `lossTrades` / `evenTrades` | `int` | Closed trades with a profit, a loss, at break-even. | | `strategy.maxContractsHeldAll` / `Long` / `Short` | `float` | Largest position held so far. | A pane with the equity curve: ```flowscope script "Equity pane" strategy(initialCapital: 10000.0, defaultQtyType: strategy.percentOfEquity, defaultQtyValue: 50.0) data chart = subscribe(data.ohlcv) pane equityPane = pane(title: "Equity", height: 0.25) plot equity = plot.line(title: "Equity", color: color.teal, width: 2, on: equityPane) on chart.close { if ta.crossover(ta.ema(chart.close, 12), ta.ema(chart.close, 26)) { strategy.entry("Long", strategy.long) } if ta.crossunder(ta.ema(chart.close, 12), ta.ema(chart.close, 26)) { strategy.close("Long") } equity.plot(strategy.equity) } ``` ## Trades {#trades} `strategy.openTrade(i)` and `strategy.closedTrade(i)` return the trade at index `i`, oldest first, or null when there is none. Both return a `StrategyTrade`: | Field | Type | Meaning | |---|---|---| | `entryId` | `string` | Id of the entry order. | | `entryPrice` | `float` | Fill price of the entry. | | `entryTime` | `time` | Open time of the bar the entry filled on. | | `entryBarIndex` | `int` | Bar index of the entry. | | `entryComment` | `string?` | Comment of the entry order. | | `size` | `float` | Contracts: positive long, negative short. | | `exitId` | `string?` | Id of the exit order; null for open trades. | | `exitPrice` | `float?` | Fill price of the exit. | | `exitTime` | `time?` | Open time of the bar the exit filled on. | | `exitBarIndex` | `int?` | Bar index of the exit. | | `exitComment` | `string?` | Comment of the exit order. | | `profit` | `float` | Net profit after commission and funding; open trades: unrealised. | | `profitPercent` | `float` | Profit relative to the entry value. | | `commission` | `float` | Commission of the trade. | | `maxRunup` | `float` | Largest unrealised profit during the trade. | | `maxDrawdown` | `float` | Largest unrealised loss during the trade, positive. | ### Reacting to a closed trade Compare `strategy.closedTrades` with the count seen last bar: ```flowscope script "Last trade" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) data chart = subscribe(data.ohlcv) state seen = 0 state losses = 0 on chart.close { if ta.crossover(chart.close, ta.sma(chart.close, 20)) { strategy.entry("Long", strategy.long) strategy.exit("Exit", "Long", profit: 300.0, loss: 150.0) } if strategy.closedTrades > seen { let last = strategy.closedTrade(strategy.closedTrades - 1) if last != null && last.profit < 0.0 { losses += 1 log.info(str.format("Loss {0} on {1}", last.profit, last.entryId)) } seen = strategy.closedTrades } } ``` ## Account values in risk logic Because the values are current, a script can size and stop on them, for example halting after a drawdown or sizing from equity. See [Risk](/docs/scripting/strategies/risk) for complete patterns. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies/position-and-trades --- # Broker emulator > When and at what price simulated orders fill, and how commission, slippage, funding and margin are applied. The broker emulator turns order calls into fills. It follows TradingView's strategy emulator and is deterministic: the same script on the same bars always produces the same trades. ## The basic cycle For every confirmed chart bar, in this order: 1. **Fill phase.** The emulator walks the bar along its price path and fills pending orders: market orders at the open, then limit, stop and exit orders where the path reaches them. 2. **Script phase.** `on chart.close` runs and sees the account after this bar's fills. It may place, modify or cancel orders. Orders placed in the script phase of bar *N* are first worked in the fill phase of bar *N + 1*; market orders fill at its open plus slippage. ### Timeline example A 1-hour chart with `slippage: 1`, tick size 0.1, a long entry with a 300-tick stop: | Bar | Open | High | Low | Close | What happens | |---|---|---|---|---|---| | 10:00 | 61,950.0 | 62,120.0 | 61,900.0 | 62,080.0 | `on chart.close`: the EMAs cross up. `strategy.entry("Long", strategy.long)` and `strategy.exit("SL", "Long", loss: 300.0)` are queued. | | 11:00 | 62,075.0 | 62,300.0 | 62,050.0 | 62,250.0 | The market buy fills at 62,075.0 + 0.1 = **62,075.1**. The stop is armed at 62,075.1 − 30.0 = **62,045.1**; the low of 62,050.0 stays above it. | | 12:00 | 62,240.0 | 62,260.0 | 61,980.0 | 62,010.0 | Path: open → high → low → close. On the way down the stop triggers and fills at 62,045.1 − 0.1 = **62,045.0**. | Exits are live from the moment the entry fills, including the rest of the entry bar: had the 11:00 low been 62,040.0, the stop would have filled on the entry bar. ## processOrdersOnClose With `processOrdersOnClose: true`, orders placed in `on chart.close` are worked at that bar's close instead of the next open: market orders fill at the close plus slippage. | Setting | Signal at bar N's close fills at | |---|---| | `false` (default) | Open of bar N + 1 | | `true` | Close of bar N | On liquid 24/7 crypto markets the next open is usually within a tick of the close, so the difference is small, but around news it is not. ## The intrabar path {#intrabar-path} A bar reports four prices. The emulator assumes the path **open → nearer extreme → farther extreme → close**: when the high is at least as close to the open as the low, price goes up first, otherwise down first. Orders are checked along each segment in the order the path reaches them, so for a bracket the leg on the first segment wins. `useBarMagnifier` would walk lower-timeframe bars inside each bar instead. Charts do not provide them yet, so strategies on charts always use the four-point path. ## Limit orders {#limit} A limit order fills when price reaches the limit: a buy limit at 3,000.0 fills when the path touches 3,000.0. It fills at the limit price, or at the open when the bar opens beyond it (a better price). Limits never slip. `backtestFillLimitsAssumption: n` asks for more: price must trade `n` ticks through the limit before it fills, at the limit price. This approximates waiting in the queue behind other orders. ## Stop orders and gaps {#stop} A stop triggers when price trades at or beyond it and fills at the stop price plus slippage. When a bar opens beyond the stop (a gap), it fills at the open plus slippage, worse than the stop. ## Slippage and commission | Fill | Slippage | Commission | |---|---|---| | Market | `slippage` ticks against you | Yes | | Stop, stop-loss, trailing stop | `slippage` ticks against you | Yes | | Limit, take-profit | None | Yes | | Margin call | None | Yes | Commission follows `commissionType` and `commissionValue` and is part of each trade's profit. ## Funding on perpetuals {#funding} With `fundingCosts: true` (the default), a position held through a funding settlement pays or receives `rate × position value` at the open of the first bar at or after the settlement: longs pay a positive rate and shorts receive it. The rates are the ones the data hub recorded just before each settlement; aggregated markets use the open-interest weighted rate of their venues. Funding is part of each trade's profit and of the Strategy Tester's **Funding paid**. ## Margin and liquidation {#margin} With `marginLong` or `marginShort` below 100 % (or `leverage` above 1), positions may be worth more than equity. An entry the account cannot fund is skipped. On every price the emulator checks that equity covers the margin of the position; when it no longer does, it liquidates part of the position, as TradingView does: four times the contracts needed to cover the shortfall, at that price. Each event is a **Margin call** fill and counts in the tester's margin calls. `strategy.marginLiquidationPrice` tells where the next one would start. ## Aggregated markets {#aggregated} On an aggregated market the emulator fills at the aggregated price, which no single venue offers. Treat such a backtest as a view of the market as a whole and confirm promising results on the venue you would trade. ## Repainting and look-ahead {#look-ahead} A strategy only sees the past: `on chart.close` runs after a bar is confirmed, and its orders fill on later prices. Two things still deserve care: - Signals from `on chart.update` use the forming bar, whose values change until it closes. Place orders in `on chart.close`. - Reading another timeframe's `forming` values gives the developing bar, not a confirmed one. See [repainting](/docs/scripting/guides/repainting). Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies/broker-emulator --- # Risk > Risk-based sizing, position caps, drawdown stops, daily loss limits and cooldowns, written in the strategy script. Risk rules are part of the script: compute the size, check the account and decide whether to trade. The examples below combine the order functions with the values from [Position and trades](/docs/scripting/strategies/position-and-trades). ## Sizing by risk {#risk-sizing} Risk a fixed share of equity per trade: the size is the money at risk divided by the distance to the stop. ```flowscope script "Risk 0.5 % per trade" strategy(initialCapital: 10000.0, leverage: 5.0) input ( riskPct = input.float(0.5, title: "Risk per trade (% equity)", min: 0.05, max: 5.0) stopAtr = input.float(1.5, title: "Stop distance (ATR)", min: 0.2) ) data chart = subscribe(data.ohlcv) on chart.close { let atr = chart.atr(14) let fast = ta.ema(chart.close, 10) let slow = ta.ema(chart.close, 30) if atr != null && atr > 0.0 && ta.crossover(fast, slow) { let stopDistance = atr * stopAtr let qty = strategy.equity * riskPct / 100.0 / stopDistance strategy.entry("Long", strategy.long, qty) strategy.exit("Long SL", "Long", loss: math.round(stopDistance / context.tickSize)) } } ``` A tight stop can ask for more than the margin allows; the emulator then skips the entry rather than shrink it. Cap the size by what the account can carry: ```flowscope script "Risk sizing with a cap" strategy(initialCapital: 10000.0, leverage: 3.0) data chart = subscribe(data.ohlcv) on chart.close { let atr = chart.atr(14) let close = chart.close if atr != null && close != null && atr > 0.0 && ta.crossover(close, ta.sma(chart.close, 50)) { let byRisk = strategy.equity * 0.01 / (atr * 2.0) // 3x leverage: at most three times equity in notional, with some room. let byMargin = strategy.equity * 3.0 * 0.95 / close strategy.entry("Long", strategy.long, math.min(byRisk, byMargin)) strategy.exit("Long SL", "Long", stop: close - atr * 2.0) } } ``` ## Position cap {#max-position} Only add while the position is below a limit: ```flowscope script "Pyramid with a cap" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 0.5, pyramiding: 4) input maxSize = input.float(1.5, title: "Largest position", min: 0.1) data chart = subscribe(data.ohlcv) on chart.close { let ema = ta.ema(chart.close, 20) if ema != null && chart.close > ema && strategy.positionSize + 0.5 <= maxSize { strategy.entry("Add", strategy.long) } if ema != null && chart.close < ema { strategy.closeAll() } } ``` ## Drawdown stop {#max-drawdown} Track the equity peak and stop trading after a drawdown: ```flowscope script "Halt after 10 % drawdown" strategy(initialCapital: 10000.0, defaultQtyType: strategy.percentOfEquity, defaultQtyValue: 50.0) data chart = subscribe(data.ohlcv) state peak = 0.0 state halted = false on chart.close { peak = math.max(peak, strategy.equity) if !halted && strategy.equity < peak * 0.9 { halted = true strategy.closeAll(comment: "Drawdown halt") strategy.cancelAll() } if !halted && ta.crossover(ta.ema(chart.close, 9), ta.ema(chart.close, 21)) { strategy.entry("Long", strategy.long) } if ta.crossunder(ta.ema(chart.close, 9), ta.ema(chart.close, 21)) { strategy.close("Long") } } ``` ## Daily loss limit {#daily-loss} Remember equity at the start of each UTC day and stand aside for the rest of the day after losing 2 %: ```flowscope script "Daily loss limit" strategy(initialCapital: 10000.0, defaultQtyType: strategy.percentOfEquity, defaultQtyValue: 30.0) data chart = subscribe(data.ohlcv) state day: time? = null state dayEquity = 0.0 on chart.close { let t = chart.time if t != null { let today = time.dayStart(t) if day != today { day = today dayEquity = strategy.equity } } let stopped = strategy.equity < dayEquity * 0.98 if stopped && strategy.positionSize != 0.0 { strategy.closeAll(comment: "Daily limit") } if !stopped && ta.crossover(chart.close, ta.ema(chart.close, 50)) { strategy.entry("Long", strategy.long) strategy.exit("Long SL", "Long", loss: 200.0) } } ``` ## Cooldown after a loss {#cooldown} Wait a number of bars after a losing trade: ```flowscope script "Cooldown" strategy(defaultQtyType: strategy.fixed, defaultQtyValue: 1.0) input cooldownBars = input.int(12, title: "Bars to wait after a loss", min: 1) data chart = subscribe(data.ohlcv) state seen = 0 state wait = 0 on chart.close { if strategy.closedTrades > seen { let last = strategy.closedTrade(strategy.closedTrades - 1) if last != null && last.profit < 0.0 { wait = cooldownBars } seen = strategy.closedTrades } if wait > 0 { wait -= 1 } else if ta.crossover(chart.close, ta.sma(chart.close, 20)) { strategy.entry("Long", strategy.long) strategy.exit("Exit", "Long", profit: 250.0, loss: 120.0) } } ``` ## Choosing limits - Size from the stop distance, not from a fixed share of equity, so every trade risks the same. - Test limits on a period you did not tune on, for example another market. - Commission and slippage grow with trade count: set them before judging a high-frequency strategy. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies/risk --- # Strategy Tester > The bottom panel that reports a strategy's backtest and paper-trading results, with every metric defined. The Strategy Tester is a panel at the bottom of the terminal window. It shows the result of the strategy on the active chart: the equity curve, a performance summary, every trade, and the settings the run used. It updates as the backtest completes and keeps updating while the strategy paper-trades live. ## Opening the panel 1. Open a chart for the market and timeframe you want to test. 2. Open the script editor (**Script editor** in the bottom bar), pick or write a strategy and press **Add to chart**, or click it under **My scripts** in the indicators dialog. 3. The Strategy Tester opens at the bottom of the window with the result of the run. **Strategy tester** in the bottom bar shows and hides the panel. Drag its top edge to resize it; close it with × on the right. Hiding or removing the strategy on the chart removes its report. On the chart itself, each fill is marked with an arrow and a label carrying the order id and size, and a horizontal line marks the average entry price while a position is open. ## The header | Part | What it does | |---|---| | Report / List of trades | The two icons at the left switch between the report and the list of trades | | Strategy | Its name; with several strategies on the chart, a selector | | Test period | The dates tested; the menu picks the [period](#range) | | Initial capital | `100K USD`; the menu offers 1K to 10M or any amount | | ⚙ | The strategy's settings on the [Properties](#properties) tab | ## The report The report reads from top to bottom, as TradingView's Strategy Report: - **Key data**: total P&L, max drawdown, profitable trades, profit factor, and the open trade's P&L if there is one. - **Performance**: the cumulative P&L after each closed trade, a dot per trade. Under the line each trade's run-up stands above zero and its drawdown hangs below, bright on the side the trade ended on. A strip along the bottom marks the equity's growth (green, from a low to its next high) and decline (red). Hover a trade for its P&L, run-up, drawdown and signal. The legend shows or hides each series, including **Buy & hold** at the same exits. - **Performance analysis**, in chips: | Chip | Contents | |---|---| | Breakdown | Gross profit and loss, profit factor, commission load (commission as a share of gross profit), then **Profits and losses** by signal or by side: the gross loss left of zero, the gross profit right of it, the net part bright, the net amount at the end | | Periodic | Best, worst and average month, profitable months, a bar per month and a table of the years with each month's return | | Benchmarking | The strategy's return against buy & hold, the outperformance, Sharpe and Sortino, and both returns over time | | Margin | Average and maximum margin used, margin calls, the account size required and the return on it | | Growth and decline | Max run-up and drawdown (intrabar and close to close), their averages and durations, the return of max drawdown and CAGR | - **Trade analysis**, in chips: | Chip | Contents | |---|---| | Distribution | Expectancy, outlier P&L (trades more than three standard deviations from the average return), the largest win and loss; the returns as a histogram with the average loss and win, and winners, losers and break-evens as a ring | | Streaks | The longest and average winning and losing streaks, and every streak in order | | Time patterns | P&L by weekday and by hour of the entry, in the terminal's time zone | - **Summary**: every [metric](#metrics) for all, long and short trades. In a narrow panel the report scrolls sideways rather than squeezing its charts. ## Metric definitions {#metrics} | Metric | Definition | |---|---| | Net profit | Sum of `profit` over closed trades (after commission). Percentage: net profit / initial capital. | | Gross profit | Sum of `profit` over winning trades (`profit > 0`). | | Gross loss | Absolute sum of `profit` over losing trades (`profit ≤ 0`). | | Profit factor | Gross profit / gross loss. Shown as "—" when there are no losses. | | Max drawdown | Largest decline of equity from a running peak to a subsequent trough, with open positions marked at the worst price of each bar. Percentage: relative to that peak. | | Max run-up | Largest rise of equity from a running trough to a subsequent peak, with open positions marked at the best price of each bar. Percentage: relative to that trough. | | Sharpe ratio | (Mean monthly return − risk-free rate / 12) / standard deviation of the monthly returns. The risk-free rate is a [property](#properties), 2 % by default. | | Sortino ratio | (Mean monthly return − risk-free rate / 12) / downside deviation: the root mean square of the monthly returns below that rate. | | Total closed trades | Number of closed trades. | | Percent profitable | Winning trades / total closed trades × 100. | | Avg trade | Net profit / total closed trades. Percentage: mean of `profitPercent`. | | Avg winning trade | Gross profit / number of winning trades. | | Avg losing trade | Gross loss / number of losing trades. | | Ratio avg win / avg loss | Avg winning trade / avg losing trade. | | Largest winning trade | Maximum `profit` of any trade. | | Largest losing trade | Minimum `profit` of any trade, shown as a loss. | | Avg bars in trade | Mean number of chart bars between entry and exit fills; also shown for winners and losers separately. | | Commission paid | Sum of commission over all fills, open position included. | | Max contracts held | Maximum of `abs(strategy.position.size)` over the run. | | Open P&L | `strategy.position.openProfit` at the last bar. | | Margin calls | Number of liquidations by the margin-call rule. | | Buy & hold return | `(lastClose − firstClose) / firstClose × initialCapital`, without costs. Percentage: `(lastClose / firstClose − 1) × 100`. | ### All, Long and Short Each metric has three columns: | Column | Trades included | |---|---| | All | Every closed trade | | Long | Trades with positive `size` | | Short | Trades with negative `size` | Equity-based metrics (max drawdown, max run-up, Sharpe, Sortino, buy & hold) only exist for the account as a whole and show "—" in the Long and Short columns. > [!TIP] > A strategy whose entire net profit comes from one side is a directional bet on the > tested period. Check the Long and Short columns before trusting the All column. ## List of trades {#trades} The second icon in the header shows every trade. Each closed trade occupies two rows, exit above entry, newest first (click **Trade #** for oldest first), with these columns: | Column | Meaning | |---|---| | Trade # | Trade number, oldest first (matches `strategy.trade(index)` + 1) | | Type | Entry long / Exit long / Entry short / Exit short | | Date and time | Fill time in the terminal's time zone | | Signal | The order id (`entryId` or `exitId`, or its comment) | | Price | Fill price including slippage | | Position size | Quantity in base units, and its value at entry | | Net P&L | Net P&L of the trade after commission, and in percent of its entry value | | Run-up | `maxRunup` of the trade, and in percent | | Drawdown | `maxDrawdown` of the trade, and in percent | | Cumulative P&L | Running sum of net P&L up to this trade, and in percent of the initial capital | Open trades appear at the top marked "Open"; with a single open trade its row also shows the open P&L. Only the visible rows are drawn, so runs with many thousands of trades scroll smoothly. ### Copy as CSV **Copy as CSV** at the top right of the list puts every closed trade on the clipboard as CSV, one row per trade: trade number, direction, entry id, time and price, exit id, time and price, quantity, profit, commission, cumulative profit, run-up and drawdown. Times are ISO 8601 UTC and decimals use `.`, so it pastes straight into a spreadsheet or loads into a dataframe. ## Properties {#properties} The ⚙ in the header (or **Settings** on the strategy's legend row) opens the strategy's settings with two tabs: **Inputs**, the settings the script declares, and **Properties**, what its `strategy(...)` declaration sets, as on TradingView: | Group | Properties | |---|---| | General | Initial capital, default order size (contracts, cash or percent of equity), pyramiding, test period | | Execution | Fill orders on bar close (`processOrdersOnClose`), recalculate on every tick (`calcOnEveryTick`, live bars) | | Broker emulator | Commission (percent, per contract or per order), margin for long and short positions (shown with the leverage it means), slippage in ticks, ticks a price must trade through a limit, funding costs | | Report | Risk-free rate for the Sharpe and Sortino ratios | Each property starts at the value the code declares and keeps following the code until you change it here. Changes apply at once and the backtest re-runs; **Cancel** restores what was set when the dialog opened, **Defaults** returns every property to the code's value. The properties belong to the strategy on that chart, so the same script can run with different capital or costs on two charts. ## Test period {#range} The test period menu in the header chooses which bars are traded: | Period | Bars | |---|---| | Available chart range | Every bar the chart has loaded (the default) | | Last 7, 30, 90 or 365 days | Up to the newest bar | | Custom date range | From the start of the first day to the end of the last, `YYYY-MM-DD` | The script still runs over every loaded bar, so indicators are warmed up when the period starts; orders are only placed and filled inside it, and every figure of the report covers only the period. A position still open when a custom period ends stays open in the report. To test further back, scroll the chart left: older history loads from the data data hub ([Data](/docs/data)), and the backtest re-runs over the longer range. > [!NOTE] > History reaches back as far as the hub recorded the market; before that a chart > has no bars. Aggregated `all` markets are complete only where every venue was > recorded. For long backtests, a hub that has been recording for months gives the > most data. ## Comparing runs {#comparing} Changing an input, a property, the period or the capital re-runs the backtest immediately, and the report updates. A practical routine for comparing variants: 1. Note the properties of a baseline run and copy its trades as CSV. 2. Change one input at a time, for example the stop distance. 3. Compare net profit, max drawdown, profit factor and trade count, not net profit alone. 4. Re-check the best variant on a period you did not use while tuning, with a custom date range, another market or older history. ## Caveats - **Fills are modelled.** Limit fills require trade-through, stops slip, but queue position, partial fills, order book depth and latency are not simulated. - **Costs are yours to set.** A strategy without commission and slippage almost always looks better than it is. - **Overfitting.** Many input combinations tried on one history will find one that fits noise. Fewer inputs and out-of-sample checks help. - **Aggregated markets** fill at an aggregate price that no single venue offers; see [Broker emulator](/docs/scripting/strategies/broker-emulator#aggregated). - **Paper trading is not trading.** Live results in the tester are simulated on real data; no orders reach any exchange. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/strategies/strategy-tester --- # Examples > Complete, commented scripts for order flow, market structure, dashboards and strategies that you can paste into the editor and adapt. Every page in this section contains one complete script. You can paste it into the script editor as it is, attach it to a chart and see the result straight away. Below the script, the page walks through the parts that matter and suggests how to adapt it. The examples lean on the data that makes Flowscope different: the trade stream, volume delta, aggregated markets that merge every venue, liquidations, funding, open interest, the recorded order book and footprint volume profiles. Each script exposes its thresholds as inputs, so you can tune it in the settings without editing code. > [!TIP] > Most order-flow examples read better on an aggregated market such as BTC across all venues. Delta, CVD and large prints then cover every venue at once, instead of the share of the market that one exchange happens to see. ## Indicators ```cards [Seconds order flow](/docs/scripting/examples/seconds-flow) Normalize volume and delta on recorded 10-second bars using rolling z-scores. [Plots and panes](/docs/scripting/examples/plots-and-panes) Price trend on the chart plus CVD, open-interest change and volume delta in three sub-panes. [CVD divergence](/docs/scripting/examples/cvd-divergence) Compare confirmed price swings against cumulative delta swings and mark where they disagree. [OI regimes](/docs/scripting/examples/oi-regimes) Sort every bar into one of four price and open-interest quadrants and colour it accordingly. [VD absorption](/docs/scripting/examples/vd-absorption) Find bars where an outsized volume delta failed to move price, scaled to current volatility. [Delta turnaround 15m](/docs/scripting/examples/delta-turnaround-15m) Mark failed breakouts confirmed by volume delta and draw each trade with an ATR stop and a 3R target. [Large prints](/docs/scripting/examples/large-prints) Keep trades above a notional floor, mark them on the chart and sum a per-bar whale delta. [Intrabar order flow](/docs/scripting/examples/intrabar-order-flow) See where inside each bar the big trades landed and flag trapped buyers and sellers. [Liquidity profile](/docs/scripting/examples/liquidity-profile) Rolling POC and value area on price, with order-book imbalance by depth in a separate window. [Aggregated spot bands](/docs/scripting/examples/aggregated-spot-bands) Sum several spot exchanges' books and draw where bids or asks dominate as a tiered band below or above the price. [Liquidation magnets](/docs/scripting/examples/liquidation-magnets) Mark the levels where liquidation cascades fired and keep them until price returns. [Funding extremes](/docs/scripting/examples/funding-extremes) Turn funding into a z-score with extreme bands and alerts. [Size-band delta](/docs/scripting/examples/size-band-delta) Split delta into retail, mid-size and whale flow from the trade stream. ``` ## Structure ```cards [Swing levels](/docs/scripting/examples/swing-levels) Turn confirmed pivots into support and resistance rays that stop when price breaks them. [HTF trend filter](/docs/scripting/examples/htf-trend-filter) Only accept chart-timeframe signals that agree with confirmed 1h and 4h trend. [Market structure](/docs/scripting/examples/market-structure) Build supply and demand zones on a higher timeframe and react to breakouts on the chart. [Trading sessions](/docs/scripting/examples/trading-sessions) Shade the Asia, London and New York sessions and box each session's range. [Trend candles](/docs/scripting/examples/trend-candles) Recolour candles by trend state and mark every flip. ``` ## Dashboards ```cards [Market stats table](/docs/scripting/examples/market-stats-table) A live table of price, volume, open interest, funding and liquidations in its own window. [MTF dashboard](/docs/scripting/examples/mtf-dashboard) Trend, momentum and alignment across three timeframes in one table. [Viewport HUD](/docs/scripting/examples/viewport-hud) A compact market card pinned to a corner of the chart while you pan and zoom. ``` ## Strategies ```cards [EMA and ATR strategy](/docs/scripting/examples/ema-atr-strategy) A trend-following strategy with ATR-sized stops, targets and a trailing exit. [CVD breakout strategy](/docs/scripting/examples/cvd-breakout-strategy) Trade range breakouts only when cumulative delta confirms the move. [Liquidation sweep reversal](/docs/scripting/examples/liquidation-sweep-reversal) Fade a sweep into a liquidation-model cluster once liquidations spike, open interest falls and the delta turns. [Wall absorption](/docs/scripting/examples/wall-absorption) Trade an order-book wall that market orders hit without getting through, stop behind it. ``` ## Before you adapt an example - Read [data and update events](/docs/scripting/guides/data-and-events) first. Most examples combine several subscriptions, and the rules for confirmed versus forming values decide whether a signal repaints. - When an example reads a higher timeframe, [multiple timeframes](/docs/scripting/guides/multiple-timeframes) explains why it uses the last confirmed period. - Market data can be missing for a bar. The scripts check for `null` before they compute; [missing values](/docs/scripting/guides/missing-values) explains the pattern. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples --- # Plots and panes > Lay out a price trend on the chart and CVD, open-interest change and volume delta in three stacked sub-panes from one script. This script builds a compact order-flow layout. On the price pane you see a fast and a slow EMA with the space between them shaded. Below the chart, three panes show cumulative volume delta that restarts every day, the bar-by-bar change in open interest, and the volume delta of each bar. It is a good template whenever you want several related series in one indicator: one script, one set of inputs, and every pane removed together when you remove the script. ```flowscope title="plots-and-panes.fs" script "Flow layout" input ( fastLen = input.int(21, title: "Fast EMA", min: 2, max: 200) slowLen = input.int(55, title: "Slow EMA", min: 3, max: 400) dailyReset = input.bool(true, title: "Restart CVD every day") ) data ( chart = subscribe(data.ohlcv) oi = subscribe(data.oi) vd = subscribe(data.vd) ) pane ( cvdPane = pane(title: "CVD", height: 0.2) oiPane = pane(title: "OI change", height: 0.15) vdPane = pane(title: "Volume delta", height: 0.15) ) plot ( fast = plot.line(title: "Fast EMA", color: color.blue, width: 2) slow = plot.line(title: "Slow EMA", color: color.gray, width: 2) cvdLine = plot.line(title: "CVD", color: color.cyan, width: 2, on: cvdPane) oiBars = plot.histogram(title: "OI change", on: oiPane) vdBars = plot.histogram(title: "Delta", on: vdPane) ) fill trendFill = fill.between(fast, slow, color: color.withAlpha(color.blue, 40)) state previousTime: time? = null // Price pane: the trend lines follow the forming candle. on chart.update { let f = ta.ema(chart.close, fastLen) let s = ta.ema(chart.close, slowLen) let up = (f ?? 0.0) >= (s ?? 0.0) fast.plot(f, color: up ? color.green : color.red) slow.plot(s) } // CVD pane: sum each bar's delta, restarting on a new day. on vd.close { let t = vd.time ?? time.fromUnixSeconds(0) let newDay = dailyReset && time.isNewPeriod(t, previousTime, 1D) cvdLine.plot(ta.cum(vd.close, reset: newDay)) previousTime = t } // OI pane: change against the previous confirmed period. on oi.close { let change = ta.change(oi.close) let shade = (change ?? 0.0) >= 0.0 ? color.green : color.red oiBars.plot(change, color: color.withAlpha(shade, 200)) } // Delta pane: the close of the volume-delta bar is the bar's delta. on vd.update { let delta = vd.close vdBars.plot(delta, color: (delta ?? 0.0) >= 0.0 ? color.green : color.red) } ``` ## How it works ### Declare every surface up front Panes and plots are declarations, not statements. The grouped `pane ( ... )` block creates three panes, and each plot names its target with `on:`. A plot without `on:` lands on the price pane. `height` is a share of the chart's height. Three panes at 0.2, 0.15 and 0.15 leave half of the space for price. The order of the declarations is the order of the panes from top to bottom; in the chart you can drag the dividers, and the script remembers the sizes. ### One handler per source Each subscription has its own handlers. `ta.change(oi.close)` in `on oi.close` compares the confirmed open interest with the one before it; if either side is missing the result is `null`, and the histogram shows a gap rather than a misleading zero. Keeping sources in separate handlers means you never have to think about which subscription's bar is confirmed at a given moment. If you need to combine two sources in one calculation, read [data and update events](/docs/scripting/guides/data-and-events) first. ### Accumulate with a reset `ta.cum(value, reset:)` keeps a running sum and starts again from the current value when `reset` is true. `time.isNewPeriod` tells when a bar opens a new day; the previous bar's time lives in `state` because the handler needs it on the next call. ### Colour per point Every `plot(...)` call accepts an optional `color:`. The fast EMA switches colour by its position against the slow EMA, and the histograms colour each bar by sign. The fill between the two EMAs is declared once with `fill.between` and needs no handler code. ## Adapting it - **Liquidations instead of OI.** Subscribe to `data.stat` and plot `stats.sellLiq` as a positive and `stats.buyLiq` as a negative histogram; see [data and update events](/docs/scripting/guides/data-and-events#statistics-liquidations-and-funding). - **Weekly CVD.** Replace `1D` with `1W` in `time.isNewPeriod`. - **Hide a series without removing it.** Add an `input.bool` and plot `show ? value : null`. The plot stays declared, so the layout does not change. ## Related - [Inputs and visuals](/docs/scripting/guides/inputs-and-visuals) - [Custom axes and charts](/docs/scripting/guides/custom-axes-and-charts) - [`plot` reference](/docs/scripting/reference/catalog/plot), [`data` reference](/docs/scripting/reference/catalog/data) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/plots-and-panes --- # CVD divergence > Compare confirmed price swings against cumulative volume delta and mark the places where price makes a new extreme that delta does not confirm. A divergence between price and cumulative volume delta is one of the most common order-flow reads. Price prints a higher high, but aggressive buying at that high was weaker than at the previous one. Or price makes a lower low while sellers hit the bid less than before. This script finds confirmed swing highs and lows on price, reads the CVD value at each swing, and compares it with the previous swing of the same kind. When price and CVD disagree, it connects the two swings on the chart, labels the divergence, and puts a marker in the CVD pane. Nothing is drawn until a swing is confirmed, so the marks do not move once they appear. ```flowscope title="cvd-divergence.fs" script "CVD divergence" input ( left = input.int(5, title: "Swing bars left", min: 1, max: 30) right = input.int(3, title: "Swing bars right", min: 1, max: 20) maxGap = input.int(80, title: "Max bars between swings", min: 5, max: 400) minMovePct = input.float(0.05, title: "Min price difference (%)", min: 0.0, max: 5.0) ) data ( chart = subscribe(data.ohlcv) cvd = subscribe(data.cvd) ) type Swing { index: int at: time price: float delta: float } type Divergence { kind: string price: float } state ( lastHigh: Swing? = null lastLow: Swing? = null count = 0 lines = entities.linePool(max: 200) labels = entities.labelPool(max: 200) ) pane cvdPane = pane(title: "CVD", height: 0.25) plot ( cvdLine = plot.line(title: "CVD", color: color.cyan, width: 2, on: cvdPane) bearMark = plot.marker(title: "Bearish divergence", color: color.red, shape: shape.down, on: cvdPane) bullMark = plot.marker(title: "Bullish divergence", color: color.green, shape: shape.up, on: cvdPane) ) alert divergenceAlert = alert(title: "CVD divergence", onClose: true, payload: Divergence) on chart.close { let ph = ta.pivotHigh(chart.high, left, right) let pl = ta.pivotLow(chart.low, left, right) let d = cvd.close cvdLine.plot(d) // Values of the bar that just became a swing, `right` bars ago. let swingTime = chart.time[right] let swingDelta = cvd.close[right] let swingIndex = chart.index - right let up = 1.0 + minMovePct / 100.0 let down = 1.0 - minMovePct / 100.0 // Copy state to locals so null checks narrow them. let prevHigh = lastHigh let prevLow = lastLow let isBear = ph != null && prevHigh != null && swingDelta != null && swingIndex - prevHigh.index <= maxGap && ph > prevHigh.price * up && swingDelta < prevHigh.delta let isBull = pl != null && prevLow != null && swingDelta != null && swingIndex - prevLow.index <= maxGap && pl < prevLow.price * down && swingDelta > prevLow.delta if isBear && ph != null && prevHigh != null && swingTime != null { count += 1 lines.get(count).set(prevHigh.at, prevHigh.price, swingTime, ph, width: 2.0, color: color.red) labels.get(count).set(swingTime, ph, "Bear div", color: color.red) divergenceAlert.trigger(Divergence { kind: "bearish", price: ph }) } if isBull && pl != null && prevLow != null && swingTime != null { count += 1 lines.get(count).set(prevLow.at, prevLow.price, swingTime, pl, width: 2.0, color: color.green) labels.get(count).set(swingTime, pl, "Bull div", color: color.green) divergenceAlert.trigger(Divergence { kind: "bullish", price: pl }) } bearMark.plot(isBear ? d : null) bullMark.plot(isBull ? d : null) // Every new swing replaces the previous one of its kind. if ph != null && swingTime != null && swingDelta != null { lastHigh = Swing { index: swingIndex, at: swingTime, price: ph, delta: swingDelta } } if pl != null && swingTime != null && swingDelta != null { lastLow = Swing { index: swingIndex, at: swingTime, price: pl, delta: swingDelta } } } ``` ## How it works ### Swings are confirmed late on purpose `ta.pivotHigh(chart.high, left, right)` returns a value only when the bar `right` bars ago is higher than the `left` bars before it and the `right` bars after it. The swing is therefore known `right` bars after it happened. Every value that belongs to the swing is read with the same offset: ```flowscope let swingTime = chart.time[right] let swingDelta = cvd.close[right] let swingIndex = chart.index - right ``` This delay is what keeps the script from repainting. A larger `right` gives you fewer, more reliable swings and later signals. For more on the trade-off, see [repainting](/docs/scripting/guides/repainting). ### Reading CVD from the chart's handler The script runs in `on chart.close`. The CVD subscription uses the chart's timeframe, so when a chart bar closes, `cvd.close` is the CVD of that same confirmed period, and `cvd.close[right]` is the CVD at the swing bar. Both `ta.pivotHigh` and `ta.pivotLow` are called on every bar, outside any `if`, so their history stays in step with the chart. ### Remembering the previous swing The last swing high and swing low live in `state` as nullable records. A null check does not narrow a `state` value, so the handler copies both to locals first. Inside a chain of `&&`, every check narrows the locals for the rest of the chain. Each continued line follows a trailing `&&`. At the end of the handler, the new swing always replaces the old one, whether or not it formed a divergence. Swings further apart than `maxGap` bars are not compared, which avoids linking a swing to a stale one from a different market phase. ### The divergence rule A bearish divergence is a higher price swing (by at least `minMovePct`) with a lower CVD reading than the previous swing high. A bullish divergence is the mirror image on swing lows. The script draws a line between the two price swings and a label on the newer one. Each divergence gets its own key in the line and label pools from a counter; the pools hold 200 entries each, so on a long history the oldest drawings are recycled. ### Markers and alerts The markers in the CVD pane appear on the bar where the divergence was confirmed, not on the swing bar. The alert uses `onClose: true` and a typed payload, so it fires once on the confirmed bar and tells you which kind of divergence occurred. ## Adapting it - **Hidden divergences.** Swap the comparisons: a higher swing low on price with a lower CVD low signals continuation instead of reversal. - **Large traders only.** Replace the CVD with a running sum of the delta of large prints from a trades subscription, so the divergence measures whale flow. See [size-band delta](/docs/scripting/examples/size-band-delta). - **Session-anchored delta.** Sum `data.vd` with `ta.cum(vd.close, reset: ...)` restarting each day, as in [plots and panes](/docs/scripting/examples/plots-and-panes), and keep `maxGap` below a day's bars. ## Related - [Swing levels](/docs/scripting/examples/swing-levels) uses the same pivot logic for support and resistance. - [User-defined types](/docs/scripting/guides/user-defined-types), [missing values](/docs/scripting/guides/missing-values) - [`ta` reference](/docs/scripting/reference/catalog/ta), [`entities` reference](/docs/scripting/reference/catalog/entities) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/cvd-divergence --- # OI regimes > Classify every bar by the direction of price and open interest, colour the OI change by regime and get alerted when a strong regime starts. Price direction alone does not tell you who is acting. Open interest does: if price rises while open interest grows, new longs are opening. If price rises while open interest falls, shorts are closing. Combining the two directions gives four regimes that traders read every day: | Price | Open interest | Regime | Typical reading | |---|---|---|---| | Up | Up | New longs | Fresh buying, trend supported by new positions | | Down | Up | New shorts | Fresh selling, shorts pressing | | Up | Down | Short covering | Rally driven by shorts leaving | | Down | Down | Long unwind | Decline driven by longs leaving or being liquidated | This script colours the open-interest change histogram by regime, tints the price background lightly, and shows how many bars in a row the current regime has lasted. Small moves below a threshold are classified as neutral, so noise does not flip the colours. ```flowscope title="oi-regimes.fs" script "OI regimes" input ( minOiPct = input.float(0.05, title: "Min OI change (%)", min: 0.0, max: 5.0) minPricePct = input.float(0.02, title: "Min price change (%)", min: 0.0, max: 5.0) tintPrice = input.bool(true, title: "Tint price background") streakAlert = input.int(3, title: "Alert after bars in regime", min: 1, max: 20) ) data ( chart = subscribe(data.ohlcv) oi = subscribe(data.oi) ) type RegimePayload { regime: string bars: int oiChangePct: float } state ( regime: int = 0 streak: int = 0 ) pane oiPane = pane(title: "OI change by regime", height: 0.25) plot ( oiBars = plot.histogram(title: "OI change %", on: oiPane) tint = plot.bg(title: "Regime tint") ) alert regimeAlert = alert(title: "OI regime established", onClose: true, payload: RegimePayload) fn regimeOf(pricePct: float, oiPct: float, minP: float, minOi: float) -> int { if math.abs(oiPct) < minOi || math.abs(pricePct) < minP { return 0 } if pricePct > 0.0 { return oiPct > 0.0 ? 1 : 3 } return oiPct > 0.0 ? 2 : 4 } fn regimeColor(r: int) -> color { return match r { 1 => color.green 2 => color.red 3 => color.cyan 4 => color.amber _ => color.gray } } fn regimeName(r: int) -> string { return match r { 1 => "New longs" 2 => "New shorts" 3 => "Short covering" 4 => "Long unwind" _ => "Neutral" } } on chart.close { let c = chart.close let pc = chart.close[1] let o = oi.close let po = oi.close[1] let valid = c != null && pc != null && o != null && po != null && pc != 0.0 && po != 0.0 if valid && c != null && pc != null && o != null && po != null { let pricePct = (c - pc) / pc * 100.0 let oiPct = (o - po) / po * 100.0 let r = regimeOf(pricePct, oiPct, minPricePct, minOiPct) streak = r == regime ? streak + 1 : 1 regime = r let shade = regimeColor(r) oiBars.plot(oiPct, color: shade) if tintPrice && r != 0 { tint.plot(color: color.withAlpha(shade, 28)) } script.setTitle(str.format("OI regimes: {0} ({1} bars)", regimeName(r), streak)) if r != 0 && streak == streakAlert { regimeAlert.trigger(RegimePayload { regime: regimeName(r), bars: streak, oiChangePct: oiPct, }) } } } ``` ## How it works ### Percent changes make the threshold portable Open interest is measured in base units, so a raw change of 500 means something different on BTC than on a small coin. The script converts both price and OI changes into percent of the previous bar before classifying them: ```flowscope let pricePct = (c - pc) / pc * 100.0 let oiPct = (o - po) / po * 100.0 ``` The `valid` flag is `false` when any value is missing or a previous value is zero. Then nothing is plotted, so the bar shows a gap instead of a misleading zero. The `if` repeats the null checks next to `valid`, because a `bool` local does not narrow the values it was computed from. ### Same timeframe, same bar Both subscriptions follow the chart. When the chart bar closes, `oi.close` is the open interest of that same confirmed period and `oi.close[1]` the one before. The classification therefore always pairs a price bar with the OI bar of the same period. See [data and update events](/docs/scripting/guides/data-and-events) for the full rules. ### Classification in a plain function `regimeOf` has no access to state and does not draw. It only maps two numbers to a regime code from 0 to 4, which makes it easy to read and to reuse. The colour and the name come from two `match` expressions. The expression form of `match` must be exhaustive, so each ends with a `_` arm for neutral. ### Streaks Two `state` values remember the regime of the previous bar and how long it has lasted. The alert fires exactly once per regime run, on the bar where the streak reaches the configured length. `script.setTitle` keeps the current regime in the indicator's legend row, which is useful when the pane is collapsed. ### Background tint `plot.bg` colours the full height of the price pane behind the bar. A bar it is not called for stays untinted, so neutral bars and the "off" setting both leave the background alone. ## Variations - **Open interest in USD.** Multiply `oi.close` by `chart.close` before taking the change. On coins with fast price moves, the USD value moves with price, so base units usually classify more cleanly. - **Confirm with liquidations.** A long unwind with heavy long liquidations is forced selling rather than voluntary exit. Subscribe to `data.stat` and treat regime 4 as "liquidation flush" when `stats.sellLiq` exceeds a USD threshold. - **Higher timeframe regime.** Subscribe to OHLCV and OI with `timeframe: 1h` and classify in `on htfOi.close` to colour a 5-minute chart by the hourly regime. ## Related - [Pattern matching](/docs/scripting/guides/pattern-matching), [functions](/docs/scripting/guides/functions) - [Plots and panes](/docs/scripting/examples/plots-and-panes) for a plain OI change pane. - [`data` reference](/docs/scripting/reference/catalog/data), [`plot` reference](/docs/scripting/reference/catalog/plot), [`script` reference](/docs/scripting/reference/catalog/script) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/oi-regimes --- # VD absorption > Detect bars where an outsized volume delta failed to move price, using thresholds that scale with recent delta and volatility. Absorption is what happens when aggressive orders hit a wall of passive liquidity. Buyers lift the offer bar after bar, the delta is strongly positive, and yet price barely rises, because a large seller keeps refilling the ask. The same happens in reverse at a bid that will not break. This script flags those bars. It compares each bar's volume delta with the typical delta of recent bars, and the bar's price move with the current ATR. When the delta is extreme but the move is small or even against the delta, the bar gets a marker and a short zone that shows where the passive side defended. A sub-pane shows the delta as a multiple of its recent average, with the threshold drawn in. ```flowscope title="vd-absorption.fs" script "VD absorption" input ( avgLen = input.int(50, title: "Delta average length", min: 5, max: 500) deltaMult = input.float(2.5, title: "Delta threshold (x average)", min: 1.0, max: 10.0) atrLen = input.int(14, title: "ATR length", min: 2, max: 100) maxMoveAtr = input.float(0.25, title: "Max move with delta (x ATR)", min: 0.0, max: 2.0) zoneBars = input.int(20, title: "Zone length (bars)", min: 1, max: 200) ) data ( chart = subscribe(data.ohlcv) vd = subscribe(data.vd) ) type AbsorptionPayload { side: string delta: float price: float } state ( zoneCount = 0 zones = entities.boxPool(max: 300) ) pane ratioPane = pane(title: "Delta / average", height: 0.2) plot ( ratio = plot.histogram(title: "Delta ratio", on: ratioPane) upper = plot.line(title: "Upper threshold", color: color.gray, style: linestyle.dashed, on: ratioPane) lower = plot.line(title: "Lower threshold", color: color.gray, style: linestyle.dashed, on: ratioPane) buyAbsorbed = plot.marker(title: "Buying absorbed", color: color.red, shape: shape.down, size: 4.0) sellAbsorbed = plot.marker(title: "Selling absorbed", color: color.green, shape: shape.up, size: 4.0) ) alert absorbed = alert(title: "Absorption", onClose: true, payload: AbsorptionPayload) on chart.close { let d = vd.close let absDelta = d != null ? math.abs(d) : null let avgAbs = ta.sma(absDelta, avgLen) let atr = chart.atr(atrLen) let o = chart.open let h = chart.high let l = chart.low let c = chart.close let t = chart.time upper.plot(deltaMult) lower.plot(0.0 - deltaMult) if d != null && avgAbs != null && atr != null && o != null && h != null && l != null && c != null && t != null && avgAbs > 0.0 { let r = d / avgAbs let move = c - o let limit = maxMoveAtr * atr // Heavy buying, but the close did not get far above the open. let buysAbsorbed = r >= deltaMult && move <= limit // Heavy selling, but the close did not get far below the open. let sellsAbsorbed = r <= -deltaMult && move >= -limit let shade = buysAbsorbed || sellsAbsorbed ? color.amber : (d >= 0.0 ? color.green : color.red) ratio.plot(r, color: shade) buyAbsorbed.plot(buysAbsorbed ? h + atr * 0.3 : null) sellAbsorbed.plot(sellsAbsorbed ? l - atr * 0.3 : null) if buysAbsorbed || sellsAbsorbed { zoneCount += 1 let tone = buysAbsorbed ? color.red : color.green // The defended side: the upper half for absorbed buying, the lower half for absorbed selling. let mid = (h + l) / 2.0 let top = buysAbsorbed ? h : mid let bottom = buysAbsorbed ? mid : l zones.get(zoneCount).set(t, top, t + time.bars(zoneBars), bottom, color: color.withAlpha(tone, 40), borderWidth: 1.0, borderColor: color.withAlpha(tone, 160)) absorbed.trigger(AbsorptionPayload { side: buysAbsorbed ? "buying absorbed" : "selling absorbed", delta: d, price: c, }) } } } ``` ## How it works ### Delta from the intrabar path `data.vd` describes how the bar's delta travelled from zero to its final value: `vd.high` is the most positive delta reached inside the bar, `vd.low` the most negative, and `vd.close` the delta at the close. Absorption is about the net result, so the script reads `vd.close`. Because the subscription follows the chart timeframe, `vd.close` in `on chart.close` belongs to the same confirmed bar as the candle. ### Scale-free thresholds A fixed delta threshold would fire constantly on BTC during the US open and never on a quiet weekend. The script measures delta against its own recent average and price moves against ATR: ```flowscope let r = d / avgAbs let move = c - o let limit = maxMoveAtr * atr ``` `r` reads as "this bar's delta was 3.1 times a normal bar". `limit` is the largest move in the direction of the delta that still counts as "did not move". Both `ta.sma` and `chart.atr` are called before any branch, on every bar, so they warm up and stay in step. `chart.atr` is a method of the candle subscription, because it needs high, low and close. ### Classifying the bar Buying is absorbed when the ratio is above the threshold and the close stayed within `limit` above the open, or fell. Selling is absorbed in the mirror case. A bar that matches is coloured amber in the ratio pane, so you can scan the pane for clusters. ### Zones Each absorption bar gets a box that starts at the bar and extends `zoneBars` bars to the right: `time.bars(n)` is the duration of `n` chart bars. The box covers the half of the bar where the passive side defended: the upper half for absorbed buying (a seller sat above), the lower half for absorbed selling. Boxes are keyed by a counter. The pool keeps up to 300 boxes, and the oldest are recycled after that. ## Adapting it - **Use the intrabar extreme.** Replace `vd.close` with `vd.high` for buying and `vd.low` for selling to catch bars where a burst of aggression was absorbed and then faded before the close. - **Big traders only.** Sum the delta of large prints from a trades subscription, as in [size-band delta](/docs/scripting/examples/size-band-delta), and use it instead of `vd.close`. Absorption of large market orders is usually the more meaningful signal. - **Confirm with the book.** Add `book = subscribe(data.book)` and require `book.depth(0.25)` to show more resting value on the defended side (`askValue` above `bidValue` for absorbed buying). The book is there for the time the chart has recorded it. - **Aggregated market.** On an aggregated market, delta from every venue is summed, so one venue's taker flow cannot hide absorption on another. ## Related - [Intrabar order flow](/docs/scripting/examples/intrabar-order-flow) looks at individual trades inside the bar. - [Missing values](/docs/scripting/guides/missing-values) explains the null guard at the top of the handler. - [`data` reference](/docs/scripting/reference/catalog/data), [`ta` reference](/docs/scripting/reference/catalog/ta), [`entities` reference](/docs/scripting/reference/catalog/entities) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/vd-absorption --- # Delta turnaround 15m > Mark failed breakouts on the 15 minute BTC chart where volume delta stops pushing, and plan each trade with an ATR stop and a 3R target. A breakout that fails is one of the cleaner reversal setups: price runs past the recent high or low, the orders waiting there get taken, and the bar closes back inside the range. This script marks those bars when the volume delta agrees, and draws the trade it would take from them: entry, stop and target, each with its price. It follows the idea of *Delta Turnaround Optimized 15m* by Coach NecoKronos from the MMT community, described there as "Finds failed breakouts on 15min using volume delta reversals. Optimized for 15min TF using BTC on binanceF exchange." That script's code is not public. This one is rebuilt from its signals: the entry, stop and target rules reproduce the original's lines exactly, the trigger is a reconstruction that catches every signal the original showed between 26 September and 16 October 2026, and also some it did not. Treat it as the same setup, not the same indicator. ```flowscope title="delta-turnaround-15m.fs" script "Delta Turnaround 15m (after NecoKronos)" // Failed breakouts on the 15 minute chart, confirmed by volume delta. After // the idea of "Delta Turnaround Optimized 15m" by Coach NecoKronos on MMT, // whose code is closed: this is a reconstruction from the signals it shows, // not the original. // // A bar pokes beyond the high (or low) of the bars before it and closes // back inside: the breakout failed. The bar has to reject that extreme with // a wick, close in the turnaround's direction, carry normal volume, and its // delta must not keep pushing the breakout. Then the script marks the bar // and plans a trade from its close: // // entry = close of the signal bar // stop = entry -/+ 4 x ATR(14) (Wilder ATR) // target = entry +/- 3 x the stop distance (3R) // // The entry, stop and target lines run until price reaches the stop or the // target, or the next signal replaces the trade. Made for BTC on Binance // futures at 15m like the original; other charts still run, with a note in // the legend. input ( input.tab(title: "Settings") input.section(title: "Breakout") lookback = input.int(5, title: "Breakout lookback (bars)", min: 2, max: 50, description: "The bar must poke beyond the highest high (short) or lowest low (long) of this many bars before it and close back inside.") minPoke = input.float(0.14, title: "Poke beyond the extreme (ATR)", min: 0.0, max: 5.0, description: "How far the wick must reach past the old extreme, in ATRs.") minWick = input.float(0.39, title: "Rejection wick (ATR)", min: 0.0, max: 5.0, description: "The wick on the breakout side, in ATRs.") minBody = input.float(0.03, title: "Close in turnaround direction (ATR)", min: -1.0, max: 5.0, description: "The body must point the way of the trade by at least this much: a red bar for shorts, a green bar for longs.") input.section(title: "Delta and volume") maxPush = input.float(0.065, title: "Delta still pushing (share of volume)", min: 0.0, max: 1.0, description: "The bar's delta may lean towards the breakout by at most this share of its volume. 0.065 = up to 6.5 % net buying on a short signal.") minVolume = input.float(0.63, title: "Volume (x 20-bar average)", min: 0.0, max: 10.0, description: "The bar's volume against the average of the 20 bars before it.") input.section(title: "Trade") atrLen = input.int(14, title: "ATR length", min: 1, max: 200) stopAtr = input.float(4.0, title: "Stop (ATR)", min: 0.1, max: 20.0) reward = input.float(3.0, title: "Target (R)", min: 0.1, max: 20.0) showTrade = input.bool(true, title: "Show entry, stop and target") longColor = input.color(#00e676, title: "Long") shortColor = input.color(#ff1744, title: "Short") input.tab(title: "About") input.section(title: "What it does", description: "Marks failed breakouts on 15 minute BTC charts: a bar runs past the recent high or low, gets rejected and closes back inside while its volume delta stops pushing. A green dot under the bar is a long, a red dot over it a short.") input.section(title: "The trade", description: "Entry is the close of the signal bar. The stop sits 4 ATR away, the target 3 times the stop distance. The lines end where price reaches one of them or where the next signal replaces the trade.") input.section(title: "Origin", description: "After the idea of Delta Turnaround Optimized 15m by Coach NecoKronos (MMT community). Its code is not public; the entry, stop and target rules match the original's lines exactly, the trigger is a reconstruction and fires more often than the original.") ) data chart = subscribe(data.ohlcv) type Signal { side: string entry: float stop: float target: float } state ( prevHigh: float? = null prevLow: float? = null prevAvgVol: float? = null // The trade being drawn: 1 long, -1 short, 0 none. dir = 0 entry = 0.0 stop = 0.0 target = 0.0 since: time? = null nextId = 0 dots = entities.markerPool(max: 500) lines = entities.linePool(max: 1500) ) alert longSignal = alert(title: "Delta Turnaround long", onClose: true, payload: Signal) alert shortSignal = alert(title: "Delta Turnaround short", onClose: true, payload: Signal) // Draws (or finishes) the current trade's three lines up to `t`. fn drawTrade(t: time, open: bool) { if !showTrade || since == null { return } let start = since ?? t lines.get(nextId * 3).set(start, entry, t, entry, width: 1.0, color: #ffffffb0, style: linestyle.dashed, text: str.format("Entry {0:.1}", entry)) lines.get(nextId * 3 + 1).set(start, stop, t, stop, width: 1.0, color: #ff5252, text: str.format("SL {0:.1}", stop)) lines.get(nextId * 3 + 2).set(start, target, t, target, width: 1.0, color: #26a69a, text: str.format("TP {0:.1}", target)) if !open { dir = 0 } } on chart.close { let o = chart.open let h = chart.high let l = chart.low let c = chart.close let t = chart.time let v = chart.volume let buy = chart.buyVolume let sell = chart.sellVolume // Stateful helpers run on every bar. let atr = chart.atr(atrLen) let hiNow = ta.highest(chart.high, lookback) let loNow = ta.lowest(chart.low, lookback) let avgNow = ta.sma(chart.volume, 20) let ph = prevHigh let pl = prevLow let avg = prevAvgVol prevHigh = hiNow prevLow = loNow prevAvgVol = avgNow if o == null || h == null || l == null || c == null || t == null || v == null || atr == null || atr <= 0.0 { return } // 1. The open trade: stop or target reached on this bar ends it. if dir != 0 { let stopped = dir > 0 ? l <= stop : h >= stop let reached = dir > 0 ? h >= target : l <= target if stopped || reached { drawTrade(t, false) } else { drawTrade(t, true) } } if ph == null || pl == null || avg == null || avg <= 0.0 { return } let delta = (buy ?? 0.0) - (sell ?? 0.0) let push = v > 0.0 ? delta / v : 0.0 let volOk = v >= avg * minVolume // 2. Failed breakout above: short. Below: long. let shortShape = h > ph && c < ph && (h - ph) >= minPoke * atr let shortCandle = (h - math.max(o, c)) >= minWick * atr && (o - c) >= minBody * atr let shortSig = shortShape && shortCandle && push <= maxPush && volOk let longShape = l < pl && c > pl && (pl - l) >= minPoke * atr let longCandle = (math.min(o, c) - l) >= minWick * atr && (c - o) >= minBody * atr let longSig = !shortSig && longShape && longCandle && -push <= maxPush && volOk if shortSig || longSig { // A new signal replaces the trade on the chart. if dir != 0 { drawTrade(t, false) } nextId += 1 dir = longSig ? 1 : -1 let sign = longSig ? 1.0 : -1.0 entry = c stop = c - sign * stopAtr * atr target = c + sign * stopAtr * atr * reward since = t if longSig { dots.get(nextId).set(t, l - 0.3 * atr, shape: shape.circle, color: longColor, size: 6.0) longSignal.trigger(Signal { side: "long", entry: entry, stop: stop, target: target }) } else { dots.get(nextId).set(t, h + 0.3 * atr, shape: shape.circle, color: shortColor, size: 6.0) shortSignal.trigger(Signal { side: "short", entry: entry, stop: stop, target: target }) } drawTrade(t, true) } } on chart.update { // The legend: the market note, and the trade in play. let tfOk = time.tfSeconds(context.timeframe) == 900 let marketOk = context.exchange == "binancef" && context.base == "BTC" let note = tfOk && marketOk ? "" : " · made for BTC Binance futures 15m" let trade = dir > 0 ? str.format(" · long {0:.1}", entry) : dir < 0 ? str.format(" · short {0:.1}", entry) : "" script.setTitle(str.format("Delta Turnaround 15m{0}{1}", trade, note)) } ``` ## How it works ### The failed breakout `ta.highest(chart.high, lookback)` and `ta.lowest(chart.low, lookback)` are read on every bar and kept in state, so each bar compares itself with the bars *before* it. A short needs a high above the previous high and a close back below it; a long the mirror image. The poke past the old extreme, the rejection wick and the body are measured in ATRs, so the same settings work in quiet and busy markets. ### The delta turnaround `chart.buyVolume - chart.sellVolume` is the bar's volume delta. On a short signal the buyers who drove the breakout must have stopped: the delta may lean to the buy side by at most 6.5 % of the bar's volume, and anything from neutral to net selling passes. The volume has to be at least about two thirds of the 20-bar average, so empty bars do not count. ### The trade Entry is the close of the signal bar. The stop is 4 ATR(14) away (Wilder's average, as `chart.atr` computes it) and the target three times that distance. One trade is drawn at a time: the lines run until a bar reaches the stop or the target, or until the next signal replaces the trade. If a bar touches both, the script counts the stop. ### Alerts `longSignal` and `shortSignal` fire on the close of a signal bar with the side, entry, stop and target as payload. ## Adapting it - **Fewer signals.** Raise *Rejection wick* or *Poke beyond the extreme*, or require more volume. The original fires about once every two days; the defaults here fire more often. - **Other markets.** The rules are in ATRs and shares of volume, so they carry over to other coins and timeframes, but the defaults were fitted to BTC on Binance futures at 15m. The legend says so on other charts. - **Backtest it.** Turn the script into a strategy: replace the lines with `strategy.entry` and an exit at the stop and target. See the [EMA and ATR strategy](/docs/scripting/examples/ema-atr-strategy) for the pattern. ## Related - [VD absorption](/docs/scripting/examples/vd-absorption) finds bars where a large delta failed to move price. - [Swing levels](/docs/scripting/examples/swing-levels) draws lines from a pool and ends them on a break, as this script does with its trade. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/delta-turnaround-15m --- # Large prints > Keep only the trades above a USD notional floor, mark them on the chart and fold them into a per-bar whale delta. Most of the tape is noise. The prints that move a market are the few large ones. This script reads the trade stream and keeps only prints above a notional floor. For each bar it shows the price of the largest buy and the largest sell as dots on the candle, labels prints above a second, higher threshold with their size, and plots a whale delta histogram with a running total underneath. ```flowscope title="large-prints.fs" script "Large prints" input ( minUsd = input.float(250_000.0, title: "Minimum print (USD)", min: 1_000.0) labelUsd = input.float(2_000_000.0, title: "Label prints from (USD)", min: 1_000.0) showLabels = input.bool(true, title: "Label very large prints") ) data ( chart = subscribe(data.ohlcv) tape = subscribe(data.trades) ) pane whalePane = pane(title: "Whale delta (USD)", height: 0.22) plot ( bigBuy = plot.point(title: "Largest buy", color: color.green, size: 5.0) bigSell = plot.point(title: "Largest sell", color: color.red, size: 5.0) whaleBars = plot.histogram(title: "Whale delta", on: whalePane) whaleTotal = plot.line(title: "Cumulative whale delta", color: color.amber, width: 2, on: whalePane) ) state ( buyUsd = 0.0 sellUsd = 0.0 topBuyUsd = 0.0 topSellUsd = 0.0 topBuyPrice: float? = null topSellPrice: float? = null running = 0.0 labelCount = 0 labels = entities.labelPool(max: 400) ) // Runs once for every print, oldest first. on tape.trade(tr) { let usd = tr.price * tr.size if usd >= minUsd { if tr.isBuy { buyUsd += usd if usd > topBuyUsd { topBuyUsd = usd topBuyPrice = tr.price } } else { sellUsd += usd if usd > topSellUsd { topSellUsd = usd topSellPrice = tr.price } } if showLabels && usd >= labelUsd { labelCount += 1 let text = str.format("{0:,.0} USD", usd) labels.get(labelCount).set(tr.time, tr.price, text, color: tr.isBuy ? color.green : color.red) } } } // Show the bar's large-print summary, live bar included. on chart.update { let delta = buyUsd - sellUsd whaleBars.plot(delta, color: delta >= 0.0 ? color.green : color.red) whaleTotal.plot(running + delta) bigBuy.plot(topBuyPrice) bigSell.plot(topSellPrice) } // Bank the bar into the running total and start fresh. on chart.close { running += buyUsd - sellUsd buyUsd = 0.0 sellUsd = 0.0 topBuyUsd = 0.0 topSellUsd = 0.0 topBuyPrice = null topSellPrice = null } ``` ## How it works ### Filter by notional A trade's notional is `tr.price * tr.size`. Filtering by USD rather than by base units means the same setting works across coins. If you prefer coins or contracts, `subscribe(data.trades, minSize: 5.0)` drops smaller prints before they reach the script, which is cheaper on a busy market; the floor must be known at setup, so it cannot depend on the price. On a chart, trades reach back as far as the chart keeps them in memory. Older bars have no prints and show an empty histogram. ### Trades are events, bars are summaries `on tape.trade(tr)` runs once per print. The `Trade` record has non-null fields, so `tr.price`, `tr.size`, `tr.isBuy` and `tr.time` need no null checks. State connects the two worlds: the trade handler adds into `buyUsd` and `sellUsd`, the chart's `update` handler displays the running bar, and its `close` handler moves the bar into `running` and resets the accumulators. History is replayed through the same handlers before the script goes live, so the histogram over past bars is built the same way as the live bar. ### Largest print per bar The dots show where in the bar the largest buy and the largest sell printed. A large buy at the top of a red candle reads very differently from one at the bottom. Plotting `null` (the reset value) leaves a bar without a dot when no qualifying print of that side occurred. ### Bounded labels The label pool holds 400 labels. Each very large print gets the next counter value as its key; once the pool is full, the oldest label is recycled. `str.format("{0:,.0} USD", usd)` writes the notional with thousands separators and no decimals. ## Variations - **Scale the dot.** Pass `size:` per point: `bigBuy.plot(topBuyPrice, size: math.clamp(topBuyUsd / minUsd, 3.0, 12.0))`. - **Split by size band.** Add a second floor and sum two deltas, as in [size-band delta](/docs/scripting/examples/size-band-delta). - **Alert on a print.** Declare an alert and trigger it in the trade handler when `usd` exceeds a threshold. Alerts fire on live data only and at most once per bar. ## Related - [Intrabar order flow](/docs/scripting/examples/intrabar-order-flow) annotates where inside the bar large prints landed. - [Data and update events](/docs/scripting/guides/data-and-events#trades) - [`entities` reference](/docs/scripting/reference/catalog/entities), [`str` reference](/docs/scripting/reference/catalog/str) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/large-prints --- # Intrabar order flow > Find out where inside each bar the large trades landed and flag bars where big buyers or sellers ended up trapped. A candle tells you where price went, not who pushed it there. Two bars with identical OHLC can hide opposite stories: in one, large buyers lifted the low and price followed; in the other, large buyers chased the high and the bar then collapsed underneath them. This script folds large trades into a per-bar summary. When the bar closes, it knows the volume-weighted price of the large buys and of the large sells, and where those prices sit in the bar's final range. It plots both positions in a pane from 0 (bar low) to 100 (bar high), labels busy bars with their large-print count and notional, and marks two patterns: large buying near the high of a bar that closed weak, and large selling near the low of a bar that closed strong. Those are the bars where the big side is likely trapped. ```flowscope title="intrabar-order-flow.fs" script "Intrabar order flow" input ( minUsd = input.float(100_000.0, title: "Minimum print (USD)", min: 1_000.0) labelUsd = input.float(5_000_000.0, title: "Label bars from (USD)", min: 10_000.0) edge = input.float(33.0, title: "Edge zone (% of range)", min: 5.0, max: 50.0) ) data ( chart = subscribe(data.ohlcv) tape = subscribe(data.trades) ) state ( buyUsd = 0.0 sellUsd = 0.0 buyPxUsd = 0.0 sellPxUsd = 0.0 prints = 0 labels = entities.labelPool(max: 300) ) pane posPane = pane(title: "Large-print position in bar (0 = low, 100 = high)", height: 0.22) plot ( buyPos = plot.point(title: "Large buys", color: color.green, size: 4.0, on: posPane) sellPos = plot.point(title: "Large sells", color: color.red, size: 4.0, on: posPane) mid = plot.line(title: "Mid", color: color.gray, style: linestyle.dotted, on: posPane) trappedLongs = plot.marker(title: "Trapped buyers", color: color.red, shape: shape.down) trappedShorts = plot.marker(title: "Trapped sellers", color: color.green, shape: shape.up) ) on tape.trade(tr) { let usd = tr.price * tr.size if usd >= minUsd { prints += 1 if tr.isBuy { buyUsd += usd buyPxUsd += tr.price * usd } else { sellUsd += usd sellPxUsd += tr.price * usd } } } on chart.close { let h = chart.high let l = chart.low let c = chart.close let t = chart.time mid.plot(50.0) if h != null && l != null && c != null && t != null && h > l { let range = h - l // Volume-weighted price of each side's large prints, as % of the bar range. let buyAt: float? = buyUsd > 0.0 ? (buyPxUsd / buyUsd - l) / range * 100.0 : null let sellAt: float? = sellUsd > 0.0 ? (sellPxUsd / sellUsd - l) / range * 100.0 : null let closeAt = (c - l) / range * 100.0 buyPos.plot(buyAt) sellPos.plot(sellAt) // Big buyers near the high, close in the lower half: buyers trapped. let longsTrapped = (buyAt ?? 0.0) >= 100.0 - edge && closeAt < 50.0 && buyUsd > sellUsd // Big sellers near the low, close in the upper half: sellers trapped. let shortsTrapped = sellAt != null && sellAt <= edge && closeAt > 50.0 && sellUsd > buyUsd trappedLongs.plot(longsTrapped ? h : null) trappedShorts.plot(shortsTrapped ? l : null) // Label busy bars with their large-print count and notional. let total = buyUsd + sellUsd if total >= labelUsd { let text = str.format("{0} prints, {1:,.0} USD", prints, total) labels.get(t).set(t, h, text, color: buyUsd >= sellUsd ? color.green : color.red) } } buyUsd = 0.0 sellUsd = 0.0 buyPxUsd = 0.0 sellPxUsd = 0.0 prints = 0 } ``` ## How it works ### Accumulate sums, not lists The trade handler does not store individual prints. For each side it keeps two running sums: the notional, and price times notional. Their ratio at the close is the volume-weighted average price of that side's large prints: ```flowscope buyUsd += usd buyPxUsd += tr.price * usd ``` This keeps the work per trade constant and the state small, no matter how many prints a bar receives. The final range of the bar is only known when it closes, so the position calculation happens in `on chart.close`, after which the sums are reset. ### Position in the range `(vwap - low) / range * 100` turns each side's average price into a number from 0 to 100, with a dotted line at 50. When one side had no large prints in a bar, its value is `null` and no dot is drawn. The `float?` annotations type the `null` branch of each conditional. ### Trapped participants The two patterns are simple comparisons on the positions: ```flowscope let longsTrapped = (buyAt ?? 0.0) >= 100.0 - edge && closeAt < 50.0 && buyUsd > sellUsd ``` Large buying dominated, it happened in the top `edge` percent of the range, and the bar still closed in its lower half. Someone bought size near the high and is now under water. The `?? 0.0` turns a missing value into a number that can never pass the test; the sell-side condition shows the alternative, an explicit `sellAt != null` that narrows the local. ### Labels keyed by time A pool key can be a time, so `labels.get(t)` gives every bar its own label without a counter. ### History On a chart, trades reach back as far as the chart keeps them in memory. Older bars have no prints, so their positions stay empty. ## Adapting it - **Count instead of size.** Keep `buyCount` and `sellCount` to tell a single block apart from a burst of medium prints. - **Combine with absorption.** A trapped-buyer bar that also shows [VD absorption](/docs/scripting/examples/vd-absorption) is a strong sign that a passive seller was working the high. - **Confirm with the volume profile.** Subscribe to `data.volume` and compare the large-print prices with the bar's point of control from `profile.poc()`. ## Related - [Large prints](/docs/scripting/examples/large-prints) marks the prints themselves. - [Data and update events](/docs/scripting/guides/data-and-events#trades) - [`plot` reference](/docs/scripting/reference/catalog/plot), [`entities` reference](/docs/scripting/reference/catalog/entities) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/intrabar-order-flow --- # Liquidity profile > Plot a rolling point of control and value area on price, and watch order-book imbalance at five depths in a separate window. Traded volume and resting liquidity answer two different questions. The volume profile tells you where the market has agreed on price. The order book tells you where participants are waiting right now. This script puts both in view. On the price pane you get a rolling point of control (POC) and value area computed over the last N bars, with the area between the value-area high and low shaded. The POC line changes colour as it migrates up or down. A separate window shows order-book imbalance: two lines for a near and a far depth, and a heatmap with five depth bands per bar, from red (asks dominate) to green (bids dominate). ```flowscope title="liquidity-profile.fs" script "Liquidity profile" input ( profileBars = input.int(48, title: "Profile length (bars)", min: 2, max: 500) vaPct = input.float(70.0, title: "Value area (%)", min: 50.0, max: 95.0) historyBars = input.int(120, title: "Heatmap columns (bars)", min: 10, max: 500) ) data ( chart = subscribe(data.ohlcv) vol = subscribe(data.volume) book = subscribe(data.book) ) window liq = window(title: "Book imbalance") { lines: chart.panel(title: "Imbalance, near vs far"), ladder: heatmap.new(title: "Imbalance by depth", x: axis.index, y: axis.category, capacity: historyBars * 5, min: -1.0, max: 1.0, palette: [color.red, color.rgb(30, 34, 42), color.green]), } plot ( poc = plot.line(title: "POC", color: color.amber, width: 2, style: linestyle.step) vah = plot.line(title: "VAH", color: color.withAlpha(color.purple, 160), style: linestyle.step) val = plot.line(title: "VAL", color: color.withAlpha(color.purple, 160), style: linestyle.step) near = plot.line(title: "Imbalance 0.25%", color: color.cyan, width: 2, on: liq.lines) far = plot.line(title: "Imbalance 2%", color: color.gray, on: liq.lines) ) fill valueArea = fill.between(vah, val, color: color.withAlpha(color.purple, 30)) state lastPoc: float? = null on chart.close { // Volume profile over the last `profileBars` bars. let prof = vol.profile(profileBars) let level = prof.poc() let area = prof.valueArea(vaPct) let prev = lastPoc if level != null { let p = level.price let shade = prev == null ? color.amber : p > (prev ?? p) ? color.green : p < (prev ?? p) ? color.red : color.amber poc.plot(p, color: shade) lastPoc = p } if area != null { vah.plot(area.highPrice) val.plot(area.lowPrice) } // (bid value - ask value) / total within a band around the mid: -1 .. +1. near.plot(book.imbalance(0.25)) far.plot(book.imbalance(2.0)) liq.ladder.set(chart.index, "0.1%", book.imbalance(0.1)) liq.ladder.set(chart.index, "0.25%", book.imbalance(0.25)) liq.ladder.set(chart.index, "0.5%", book.imbalance(0.5)) liq.ladder.set(chart.index, "1%", book.imbalance(1.0)) liq.ladder.set(chart.index, "2%", book.imbalance(2.0)) } ``` ## How it works ### A rolling profile in three calls `data.volume` is a snapshot source: it has no handler of its own, and you query it from another subscription's handler. On a chart, its snapshots come from the footprints of the chart's bars. `vol.profile(profileBars)` merges the last `profileBars` bar profiles into one, and the profile answers questions through methods: ```flowscope let prof = vol.profile(profileBars) let level = prof.poc() let area = prof.valueArea(vaPct) ``` `poc()` returns the `VolumeLevel` with the most volume, `valueArea(70.0)` a `VolumeArea` with `lowPrice`, `highPrice` and the volume inside. Both are `null` until footprints exist, which leaves gaps rather than invented levels. While the script runs, the chart loads footprints for the bars on screen; bars further back stay empty. ### POC migration The POC of the previous bar is kept in `state`. The nested conditional compares it with the new value and picks a colour: rising POC in green, falling in red, unchanged in amber. A POC that keeps rising while price ranges is a sign that volume is building higher. ### Imbalance from book depth `book.imbalance(pct)` compares the resting value within `pct` percent of the mid price on each side: `+1` means all of it is on the bid, `-1` all of it on the ask. Values rather than quantities keep the measure comparable across coins; pass `unit: bookunit.base` to compare quantities. `book.depth(pct)` returns the underlying `BookDepth` record when you need the sums themselves. On a chart the book comes from what the chart has recorded, the same data as the liquidity heatmap. Bars before the recording started read `null` and stay empty. ### A heatmap that scrolls The heatmap has one column per bar (`axis.index`, keyed by `chart.index`) and one row per depth band (`axis.category`, keyed by name). Its `capacity` holds `historyBars` columns of five cells; as new cells arrive, the oldest drop out. `min:`, `max:` and `palette:` pin the colours, so 0 is always the neutral middle. ## Adapting it - **Session profile.** Count the bars since the session opened with `ta.barsSince(...)` and pass that count to `vol.profile` to anchor POC and value area to the session. - **Imbalance alert.** Trigger an alert when the near imbalance stays above 0.5 for several bars while price is at the value-area low. - **Volume in a range.** `prof.summary(from: low, to: high)` sums buy and sell volume between two prices, for example around yesterday's POC. ## Related - [Custom axes and charts](/docs/scripting/guides/custom-axes-and-charts) explains windows, chart panels and heatmaps. - [Data and update events](/docs/scripting/guides/data-and-events#order-books-and-volume-profiles) - [`data` reference](/docs/scripting/reference/catalog/data), [`surfaces` reference](/docs/scripting/reference/catalog/surfaces) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/liquidity-profile --- # Aggregated spot bands > Sum the order books of several spot exchanges and draw where bids or asks dominate as a coloured band between two distances from the mid, on any chart of the coin. Perpetual futures move the price, but spot books show where buyers and sellers with real coins wait. This script sums the order books of the spot exchanges you pick (Binance and Coinbase by default) into one book and reads its balance within a few percent of the mid. When bids gain the upper hand, a band is drawn below the price between the inner and the outer edge; when asks do, above it. The colour is the tier the reading reached, three per side. It runs on any chart of the coin, including a Binance futures chart: the books come from the spot markets, whatever the chart shows. It ships with Flowscope as a community script. Classic mode keeps the settings, defaults and colours of the order-flow tool traders know it from; Adaptive mode, the default, fixes what makes the classic rule hard to use on the full book (below). ```flowscope title="aggregated-spot-bands.fs" script "Aggregated Spot Bands" // Where spot buyers and sellers stack their orders, on any chart of the coin. // // The order books of the picked spot exchanges are summed into one book // (levels added up in shared price buckets). Within the outer edge of the // mid price the script reads // // ratio = (bids - asks) / (bids + asks) // // by notional, from -1 (all asks) to +1 (all bids). Cumulative depth counts // all liquidity from the mid out to the outer edge; Differential only the // liquidity between the inner and outer edges. Book depth (levels) can // limit the count to the nearest N price levels of each side, as a data // plan with a level limit sees the book. // // Classic mode compares the ratio itself with the Lvl thresholds, as the // original indicator does. Spot books lean to one side for hours (asks // usually outweigh bids within a few percent), so fixed thresholds either // never fire on the full book or fire all the time. Adaptive mode (the // default) removes that standing lean: it scores each bar's ratio against // the last `Baseline` bars (z = (ratio - mean) / standard deviation) and // draws a band only when the book turns unusually bid- or ask-heavy. // // A bullish reading draws a band below the price, between the inner and the // outer edge (mid * (1 - inner %) to mid * (1 - outer %)), where the bid // support sits; a bearish one draws it above the price, where the ask wall // is. The band follows the mid of each bar's book, so its edges move with // the price. The colour is the tier the reading reached (Lvl 1, 2 or 3 of // its side); below Lvl 1 there is no band. On the bar where the reading // changes sign the band is left out, so it does not flicker. // // The legend reads "Spot Bands | N ex | ratio" (Adaptive: "| z score" too). input ( input.tab(title: "Settings") input.section(title: "Spot Exchanges") spotExchanges = input.exchanges(["binance", "coinbase"], title: "Spot Exchanges", minSelected: 1, filter: ["binance", "coinbase", "bybit", "okx", "kraken"], description: "Spot order books to aggregate. Depth is summed over the picked venues. Default = Binance + Coinbase. A change restarts the script.") input.section(title: "Signal") mode = input.select("Adaptive", title: "Mode", options: ["Adaptive", "Classic (MMT)"], description: "Adaptive (default): a band shows when the book is unusually bid- or ask-heavy compared with its own recent bars, so the standing ask lean of spot books does not paint a band all the time. Classic (MMT): the fixed ratio thresholds of the original indicator; with Depth Mode Auto and Book depth 250 (MMT Free) or 4096 (MMT Pro) it reads the book as the original does.") baselineMinutes = input.int(60, title: "Baseline (minutes)", min: 1, max: 1440, description: "Adaptive mode: how long a stretch the ratio is compared with, in minutes, on every chart interval (60 = the last hour: 60 bars on 1m, 360 bars on 10s, 12 on 5m). Shorter reacts faster and fires more often; longer only flags what is unusual over a longer stretch.") persistBars = input.int(1, title: "Persistence (bars)", min: 1, max: 10, description: "How many closed bars in a row a side must reach Lvl 1 before its band shows. 1 (default) = at once. 2 or more hides single-bar flashes, often orders that were pulled again (spoofs), at the cost of a later band.") input.section(title: "Band Settings") showBand = input.bool(true, title: "Show Band", description: "Show or hide the liquidity imbalance band on the chart.") drawAt = input.select("Near price", title: "Draw band", options: ["Near price", "Book edges (MMT)"], description: "Near price (default): the band hugs the candles, from half an ATR beyond the bar's low (bids) or high (asks), 3 ATR or 0.15 % of the price tall, so it stays in view at any zoom. Book edges (MMT): between the Inner and Outer Edge off the mid, where the measured orders rest, as the original draws it; 1-2.5 % away it is only in view when the chart is zoomed out.") depthMode = input.select("Auto", title: "Depth Mode", options: ["Cumulative", "Differential", "Auto"], description: "Cumulative: all liquidity from mid price out to the Outer Edge. Differential: only the liquidity between the Inner and Outer edges, so the busy book near the mid is ignored and orders stacked further out count. Auto (default): Differential in Adaptive mode, Cumulative in Classic mode (as the original).") innerEdge = input.float(1.0, title: "Inner Edge (%)", min: 0.0, max: 50.0, description: "Price distance from mid (%) where the band starts. In Differential mode liquidity tighter than this is excluded. In Cumulative mode the band still starts here visually but the depth measurement begins at mid.") outerEdge = input.float(2.5, title: "Outer Edge (%)", min: 0.1, max: 50.0, description: "Price distance from mid (%) where the band ends. This is how far into the order book the indicator looks. Wider values capture deeper walls but react slower to changes near the top of book.") bookLevels = input.int(0, title: "Book depth (levels)", min: 0, max: 100000, description: "How many price levels of each side count, nearest the best bid and ask first. 0 (default) = the full book up to the Outer Edge. 250 reproduces MMT's Free plan, 4096 its Pro plan. A level is one price bucket of the summed book ($1 on BTC), so 250 levels only reach about 0.3 % from the mid and the Outer Edge is never reached.") activation = input.float(0.0, title: "Activation (ratio)", min: 0.0, max: 1.0, description: "Classic mode: minimum imbalance ratio before any band color shows. Ratio = (bids - asks) / (bids + asks), 0.0 to 1.0. 0.0 (default) = any imbalance shows color. 0.10 = one side must hold at least 10% more liquidity.") input.tab(title: "Thresholds") input.section(title: "Adaptive Thresholds", description: "Adaptive mode: how far the ratio must stand from its baseline average, in standard deviations (σ), for each tier. 2σ lies well outside the ratio's usual swings over the baseline.") bullZ1 = input.float(2.0, title: "Bull Lvl 1 (σ)", min: 0.0, max: 20.0, description: "Bids gained this many standard deviations over the baseline: mild bullish band.") bearZ1 = input.float(2.0, title: "Bear Lvl 1 (σ)", min: 0.0, max: 20.0, sameLine: true, description: "Asks gained this many standard deviations over the baseline: mild bearish band.") bullZ2 = input.float(2.5, title: "Bull Lvl 2 (σ)", min: 0.0, max: 20.0, description: "Moderate bullish band.") bearZ2 = input.float(2.5, title: "Bear Lvl 2 (σ)", min: 0.0, max: 20.0, sameLine: true, description: "Moderate bearish band.") bullZ3 = input.float(3.0, title: "Bull Lvl 3 (σ)", min: 0.0, max: 20.0, description: "Strong bullish band.") bearZ3 = input.float(3.0, title: "Bear Lvl 3 (σ)", min: 0.0, max: 20.0, sameLine: true, description: "Strong bearish band.") input.section(title: "Bullish Thresholds (Classic)") bull1 = input.float(0.30, title: "Bull Lvl 1", min: 0.0, max: 1.0, description: "Ratio at which the Lvl 1 (mild) bullish color appears. 0.30 = bids exceed asks by 30% of total liquidity.") bull1Color = input.color(#0C5C17, title: "Bull Lvl 1 color", sameLine: true) bull2 = input.float(0.46, title: "Bull Lvl 2", min: 0.0, max: 1.0, description: "Ratio for Lvl 2 (moderate) bullish color. 0.46 = bids outweigh asks by 46% of total depth.") bull2Color = input.color(#08A212, title: "Bull Lvl 2 color", sameLine: true) bull3 = input.float(0.68, title: "Bull Lvl 3", min: 0.0, max: 1.0, description: "Ratio for Lvl 3 (strong) bullish color. 0.68 = bids dominate asks by 68% of total depth.") bull3Color = input.color(#00E1FF, title: "Bull Lvl 3 color", sameLine: true) input.section(title: "Bearish Thresholds (Classic)") bear1 = input.float(0.25, title: "Bear Lvl 1", min: 0.0, max: 1.0, description: "Ratio at which the Lvl 1 (mild) bearish color appears. 0.25 = asks exceed bids by 25% of total liquidity.") bear1Color = input.color(#670E12, title: "Bear Lvl 1 color", sameLine: true) bear2 = input.float(0.50, title: "Bear Lvl 2", min: 0.0, max: 1.0, description: "Ratio for Lvl 2 (moderate) bearish color. 0.50 = asks outweigh bids by 50% of total depth.") bear2Color = input.color(#AF07AB, title: "Bear Lvl 2 color", sameLine: true) bear3 = input.float(0.68, title: "Bear Lvl 3", min: 0.0, max: 1.0, description: "Ratio for Lvl 3 (strong) bearish color. 0.68 = asks dominate bids by 68% of total depth.") bear3Color = input.color(#00E1FF, title: "Bear Lvl 3 color", sameLine: true) input.tab(title: "About") input.group(title: "Overview") input.section(title: "What this indicator does", description: "Aggregates order book depth across the picked spot exchanges. Computes a bid/ask imbalance ratio within a configurable price range and renders it as a colored band. Band appears below price when bids dominate, above price when asks dominate.") input.section(title: "Why spot books", description: "Spot orders represent unleveraged capital. A $5M spot bid requires $5M in capital vs $500K on 10x futures. Spot book data is less open to spoofing and shows genuine positioning. The tradeoff is lower notional depth than futures.") input.section(title: "Why multi-exchange", description: "Any single venue can have thin, spoofed, or market-maker-dominated books. Aggregating across exchanges gives a more representative view of total market liquidity. The books are lined up on the first exchange's mid before they are summed, since USD and USDT prices differ by up to about $100 on BTC.") input.group(title: "Modes") input.section(title: "Adaptive (default)", description: "Spot books lean to one side for hours: within 2.5 % of the mid, asks usually outweigh bids, so the raw ratio sits around -0.1 to -0.2 for a whole day. Adaptive mode scores each bar against its own last Baseline bars: z = (ratio - average) / standard deviation. A band shows only when bids (green, below) or asks (red, above) gain unusually fast, 2σ or more. That removes the permanent red on top.") input.section(title: "Classic (MMT)", description: "The original rule: the ratio itself against fixed thresholds (Bull 0.30, Bear 0.25). With the full book the ratio rarely reaches them; with a level-limited book (Book depth 250) it reaches them in short spikes. Pick it to compare with the original indicator.") input.section(title: "Measured", description: "On one day of BTC (Binance + Coinbase spot, 1m, about 1,000 bars), Adaptive with Differential depth showed a band on 9 % of bars: 22 bullish bands, 73 % followed by a higher price 15 minutes later (77 % after 30 min); 14 bearish bands, 64 % followed by a lower price (71 % after 30 min). Classic with the full book showed none; with 250 levels, three bearish spikes. One day is a small sample: treat it as a sanity check, not a promise.") input.group(title: "Book depth and MMT plans") input.section(title: "Book depth (levels)", description: "MMT limits how many price levels of each side its plans read: 250 on Free, 4,096 on Pro. On BTC a level is $1, so 250 levels cover only about ±0.3 % around the mid and the 1-2.5 % band edges are never reached: the ratio describes the top of the book and jumps with every order there. 4,096 levels cover the whole 2.5 % range: on BTC the same as the full book. Flowscope reads the full book by default (0); set 250 or 4096 to see what each plan sees.") input.group(title: "Band Mechanics") input.section(title: "Inner / Outer Edge", description: "Inner Edge (%) and Outer Edge (%) define the price range monitored. With Inner 1% / Outer 2.5%, the indicator reads depth between 1% and 2.5% from mid price. The imbalance in this zone sets band color and intensity.") input.section(title: "Imbalance ratio", description: "Computed as (bids - asks) / (bids + asks) across all picked exchanges. Range: -1.0 (all asks) to +1.0 (all bids). The legend shows the live ratio, and in Adaptive mode its z score.") input.section(title: "Band positioning", description: "Bullish reading: band renders below price, bearish above. Near price (default) draws it right beyond the candle (from 0.5 ATR, 3 ATR or 0.15 % tall), so it is readable at a normal zoom. Book edges (MMT) draws it between the Inner and Outer Edge, where the measured bids or asks rest, as the original does.") input.section(title: "Flip suppression", description: "On the bar where the reading changes sign (bull to bear or vice versa), the band goes transparent. This prevents flicker from rapid oscillation. The band resumes on the next bar with the new direction confirmed.") input.group(title: "Depth Modes") input.section(title: "Cumulative", description: "Counts all liquidity from mid price to the Outer Edge. Gives total depth within the range. The original indicator's default; Auto uses it in Classic mode.") input.section(title: "Differential", description: "Counts only liquidity between Inner and Outer Edge, excluding everything closer to mid. Shows walls stacked 1-2.5 % away without the noise near the mid. Auto uses it in Adaptive mode, where it gave the clearest signals.") input.group(title: "Threshold System") input.section(title: "3-tier structure", description: "Bull and bear sides each have 3 levels (Lvl 1/2/3): in standard deviations in Adaptive mode (2, 2.5, 3), in ratio in Classic mode (0.30/0.46/0.68 and 0.25/0.50/0.68). As the reading crosses each level, the band moves to that tier color.") input.section(title: "Activation threshold", description: "Classic mode: minimum absolute ratio required before any color is shown. Default 0.0 (any imbalance triggers).") input.group(title: "Exchange Notes") input.section(title: "Default selection", description: "Binance + Coinbase are picked by default. Binance has the deepest spot book for most pairs. Coinbase is the primary USD venue and captures US institutional flow. Bybit, OKX and Kraken add coverage.") input.tab(title: "Tips & FAQ") input.group(title: "Setup & Interpretation") input.section(title: "Starting config", description: "Defaults: Binance + Coinbase, Adaptive, Baseline 60, Depth Mode Auto (Differential), Inner 1% / Outer 2.5%, full book. Green below price = bids building unusually fast. Red above price = asks building unusually fast.") input.section(title: "Highest-value signals", description: "Lvl 2-3 at a structurally important level (prior swing, VWAP, round number) means the order book is actively stacking at that price. Watch whether price respects it.") input.section(title: "Confluence", description: "Combines well with volume profile and liquidation maps. A Lvl 3 bid band at a high-volume node with no nearby liquidation risk is a high-confidence support signal.") input.group(title: "Signal Quality") input.section(title: "Tier intensity", description: "Lvl 1: a clear change in the book. Lvl 2: strong stacking, likely intentional. Lvl 3: an extreme one-sided change. Higher tiers carry more weight.") input.section(title: "Persistence", description: "A band held across several bars means orders are maintained or refreshed. A single-bar flash that fades at once suggests the orders were pulled: potential spoof. Persistence (bars) 2 shows a band only once its side held for two closed bars in a row.") input.section(title: "Divergence", description: "Price making new highs while ask pressure (red band) builds above = book warning of resistance ahead. Price making new lows while bid support (green band) strengthens below = bullish divergence.") input.section(title: "Why no band most of the time", description: "In Adaptive mode the band is the exception: about one bar in ten on BTC 1m. No band means the book is no more one-sided than it was over the last Baseline bars.") input.group(title: "Tuning") input.section(title: "Too noisy", description: "Adaptive: raise the Lvl 1 thresholds to 2.5σ, or lengthen the Baseline. Classic: raise Activation to 0.10-0.15 or the Lvl 1 thresholds.") input.section(title: "Too quiet", description: "Adaptive: lower Lvl 1 to 1.5σ or shorten the Baseline to 30. Classic: lower Lvl 1 (e.g. 0.15), or use Adaptive: on the full book the Classic ratio rarely reaches 0.25.") input.section(title: "Classic always red", description: "Spot books are usually ask-heavy 1-2.5 % from the mid, so Classic with Differential depth shows red on top on about four bars in ten. Use Adaptive, or Classic with Cumulative depth.") input.section(title: "Timeframe", description: "The Baseline counts bars: 60 is an hour on 1m and five hours on 5m. Keep it at about an hour to a few hours of bars. The measurements above are from 1m bars.") input.group(title: "Spot vs Futures") input.section(title: "Why spot only", description: "Spot orders require full capital commitment with no leverage. Spot book data is a cleaner directional signal with less noise from speculative positioning. The indicator excludes futures on purpose.") input.section(title: "Capital comparison", description: "A $10M futures bid at 10x leverage represents $1M in actual capital. A $5M spot bid represents $5M. Spot walls across multiple exchanges often carry more directional conviction.") input.group(title: "Troubleshooting") input.section(title: "Band not showing", description: "Check in order: (1) Show Band enabled, (2) the pair trades on the picked exchanges, (3) Adaptive: the Baseline needs that many bars of book history before the first band, (4) Classic: Activation and the Lvl 1 thresholds not above the live ratio. The title shows the live ratio.") ) data ( chart = subscribe(data.ohlcv) books = subscribe(data.book, exchange: spotExchanges) ) plot ( bullInner = plot.line(title: "Bull inner edge", color: #00000000, width: 1, showLabel: false, showValue: false) bullOuter = plot.line(title: "Bull outer edge", color: #00000000, width: 1, showLabel: false, showValue: false) bearInner = plot.line(title: "Bear inner edge", color: #00000000, width: 1, showLabel: false, showValue: false) bearOuter = plot.line(title: "Bear outer edge", color: #00000000, width: 1, showLabel: false, showValue: false) ) fill ( bullBand = fill.between(bullInner, bullOuter) bearBand = fill.between(bearInner, bearOuter) ) // The tier colour of an absolute reading, null below Lvl 1. fn tierColor(r: float, t1: float, t2: float, t3: float, c1: color, c2: color, c3: color) -> color? { if r >= t3 { return c3 } if r >= t2 { return c2 } if r >= t1 { return c1 } return null } on chart.update { let adaptive = mode == "Adaptive" let differential = depthMode == "Differential" || depthMode == "Auto" && adaptive let inner = differential ? innerEdge : 0.0 let ratio = books.imbalance(outerEdge, innerPct: inner, levels: bookLevels) let previous = books.imbalance(outerEdge, innerPct: inner, barsAgo: 1, levels: bookLevels) // Adaptive: this bar's ratio against the bars of the last // `baselineMinutes`, and the previous bar's likewise (the window of the // previous ratios). In time, so seconds charts read like the minute one. let seconds = time.durationSeconds(time.bars(1)) let bars = baselineMinutes * 60 / (seconds > 0 ? seconds : 60) let baseline = bars < 10 ? 10 : bars > 2000 ? 2000 : bars let z = ta.zscore(ratio, baseline) let zBefore = ta.zscore(previous, baseline) let mid = books.mid() let shown = ratio != null && math.isFinite(ratio) let reading = shown ? str.format("{0:.2}", ratio) : "n/a" let score = adaptive && z != null ? str.format(" | z {0:.1}", z) : "" script.setTitle(str.format("Spot Bands | {0} ex | {1}{2}", spotExchanges.len, reading, score)) let r = shown ? ratio ?? 0.0 : 0.0 let level = adaptive ? z ?? 0.0 : r let before = adaptive ? zBefore : previous let b = before ?? 0.0 // Flip suppression: no band on the bar the reading changes sign. let flipped = before != null && (level > 0.0 && b < 0.0 || level < 0.0 && b > 0.0) // Tier thresholds: standard deviations (Adaptive) or ratios (Classic). let up1 = adaptive ? bullZ1 : bull1 let up2 = adaptive ? bullZ2 : bull2 let up3 = adaptive ? bullZ3 : bull3 let down1 = adaptive ? bearZ1 : bear1 let down2 = adaptive ? bearZ2 : bear2 let down3 = adaptive ? bearZ3 : bear3 let bull = level > 0.0 ? tierColor(level, up1, up2, up3, bull1Color, bull2Color, bull3Color) : null let bear = level < 0.0 ? tierColor(-level, down1, down2, down3, bear1Color, bear2Color, bear3Color) : null let ready = adaptive ? z != null : math.abs(r) >= activation // Persistence: the side reached Lvl 1 on each of the last `persistBars` bars. let bullHeld = ta.sum(ready && shown && bull != null ? 1.0 : 0.0, persistBars) ?? 0.0 let bearHeld = ta.sum(ready && shown && bear != null ? 1.0 : 0.0, persistBars) ?? 0.0 let active = showBand && shown && mid != null && !flipped && ready let m = mid ?? 0.0 // Where the band is drawn: hugging the candle, or at the book edges. let near = drawAt == "Near price" let atr = chart.atr(14) ?? 0.0 // Near price: half an ATR off the candle, as tall as 3 ATR or 0.15 % of // the price, whichever is more, so it reads at any zoom. let byAtr = 3.0 * atr let byPrice = chart.close * 0.0015 let tall = byAtr > byPrice ? byAtr : byPrice let lowEdge = near ? chart.low - 0.5 * atr : m * (1.0 - innerEdge / 100.0) let lowFar = near ? lowEdge - tall : m * (1.0 - outerEdge / 100.0) let highEdge = near ? chart.high + 0.5 * atr : m * (1.0 + innerEdge / 100.0) let highFar = near ? highEdge + tall : m * (1.0 + outerEdge / 100.0) if active && bull != null && bullHeld >= persistBars { bullInner.plot(lowEdge) bullOuter.plot(lowFar) bullBand.fill(color: color.withAlpha(bull ?? #00000000, 240)) } else { bullInner.plot(null) bullOuter.plot(null) } if active && bear != null && bearHeld >= persistBars { bearInner.plot(highEdge) bearOuter.plot(highFar) bearBand.fill(color: color.withAlpha(bear ?? #00000000, 240)) } else { bearInner.plot(null) bearOuter.plot(null) } } ``` ## How it works ### One book from several exchanges `subscribe(data.book, exchange: spotExchanges)` takes the list the `input.exchanges` picker returns. Several exchanges' books are summed: each is shifted onto the first exchange's mid (Coinbase quotes BTC in USD, Binance in USDT, and the two sit up to about $100 apart), then their levels are added up in price buckets of the widest exchange's bin, so the queries (`mid`, `imbalance`) read one combined book. Without `symbol:`, each exchange's market of the chart's coin is read: `BTCUSDT` on Binance, `BTC-USD` on Coinbase. ### The ratio `books.imbalance(outerEdge, innerPct: inner, levels: bookLevels)` is ```text ratio = (bids - asks) / (bids + asks) ``` by notional within the outer edge of the mid, from -1 (all asks) to +1 (all bids). Cumulative depth counts everything from the mid out (`inner` is 0); Differential depth passes the inner edge, so only the liquidity between the edges counts. Depth Mode Auto picks Differential in Adaptive mode and Cumulative in Classic mode. ### Book depth: MMT Free and Pro `levels:` limits the count to the nearest price levels of each side, best bid and best ask first. 0, the default, reads the full book. MMT's plans read a fixed number of levels: 250 on Free, 4,096 on Pro. On BTC a level of the summed book is $1, so | Book depth | Reaches | What the ratio describes | |---|---|---| | 0 (full book) | the Outer Edge (2.5 %) | the whole band | | 4,096 (MMT Pro) | about ±5 % | the whole band: the same as the full book | | 250 (MMT Free) | about ±0.3 % | only the top of the book; the 1-2.5 % edges are never reached, and the ratio jumps with each order near the mid | On one day of BTC the 250-level ratio spiked past the bearish threshold on three bars and the full-book ratio never reached a threshold; that is what the original shows on the two plans. ### Classic and Adaptive Classic compares the ratio with fixed thresholds (Bull Lvl 1 0.30, Bear Lvl 1 0.25). Spot books lean to one side for hours, though: on the day we measured, asks outweighed bids within 2.5 % of the mid on almost every bar (the full-book ratio sat between -0.2 and +0.1), and between 1 % and 2.5 % the ratio was below -0.25 on four bars in ten. Fixed thresholds either never fire or paint red above the price all day. Adaptive removes that standing lean. It scores each bar's ratio against the last `Baseline` bars, ```text z = (ratio - average of the last N ratios) / their standard deviation ``` with `ta.zscore(ratio, baseline)` over the bars of the last `baselineMinutes` (60: an hour on every interval), and draws a band when bids (z ≥ 2) or asks (z ≤ -2) gained unusually fast. Lvl 2 and 3 are 2.5σ and 3σ. | Mode | Band | |---|---| | Adaptive | z ≥ Bull Lvl 1 (σ): below the price; z ≤ -Bear Lvl 1 (σ): above it | | Classic (MMT) | ratio ≥ Bull Lvl 1: below the price; ratio ≤ -Bear Lvl 1: above it; never below `Activation` | For the original's behaviour pick Classic, leave Depth Mode on Auto (Cumulative) and set Book depth to 250 (Free) or 4096 (Pro). ### What we measured One day of production data (Oct 15-16, 2026, ~17.5 hours, 1,046 one-minute bars of BTC; Binance + Coinbase spot books from the hub's minute heatmap tiles, prices from Binance BTCUSDT perpetual). A signal is the first bar of a band; "hit" means the price moved the band's way (up after green, down after red) by the stated time. Over all bars the price was higher 15 minutes later 53 % of the time. | Rule | Bars with a band | Bull signals, hit 15 / 30 / 60 min | Bear signals, hit 15 / 30 / 60 min | |---|---|---|---| | Classic, full book, Cumulative | 0 % | none | none | | Classic, 250 levels (MMT Free) | 0.3 % | none | 3: 33 / 33 / 67 % | | Classic, full book, Differential | 42 % | 6 | 55: 49 / 52 / 59 % | | Adaptive, Cumulative, 60 bars, 2σ | 7.7 % | 27: 70 / 67 / 56 % | 19: 47 / 37 / 42 % | | **Adaptive, Differential, 60 bars, 2σ (default)** | 9.0 % | 22: 73 / 77 / 73 % | 14: 64 / 71 / 71 % | | Adaptive, Differential, 120 bars, 2σ | 5.7 % | 12: 58 / 100 / 75 % | 5: 80 / 40 / 60 % | | Adaptive, 250 levels, 60 bars, 2σ | 5.6 % | 22: 59 / 59 / 64 % | 22: 36 / 36 / 41 % | Adaptive with Differential depth was the only rule whose bands on both sides pointed the right way more often than not, and it stayed so with a 30-bar baseline and with thresholds from 1.5σ to 2.5σ. Longer baselines kept the bullish bands but made the bearish ones unreliable, so the default is 60 bars on 1m, an hour; the baseline is set in minutes, so 10s charts compare with the same hour (360 bars). Requiring two bars in a row, hysteresis, or a filter on where the price stood in its recent range did not clearly improve it, so they are left out. One day is a small sample (a few dozen signals, overlapping moves, one market regime): read the table as evidence that Adaptive does not paint noise, not as a hit rate to trade on. ### The band | Reading | Band | Colour | |---|---|---| | bullish | `mid × (1 − inner %)` down to `mid × (1 − outer %)` | Bull Lvl 1, 2 or 3 | | bearish | `mid × (1 + inner %)` up to `mid × (1 + outer %)` | Bear Lvl 1, 2 or 3 | Below Lvl 1 of its side there is no band. The edges follow the mid of each bar's book, so the band moves with the price. On the bar where the reading changes sign (the ratio in Classic, z in Adaptive, compared with the previous bar's) the band is left out: it does not flicker. The previous bar's z is `ta.zscore` of `books.imbalance(..., barsAgo: 1)`, the same window one bar earlier. The band is a `fill.between` of two invisible edge lines, coloured per bar with `fill(color:)`. ### Legend `script.setTitle` writes `Spot Bands | N ex | ratio` into the legend: how many exchanges are summed and the newest ratio, and in Adaptive mode `| z` with its score. ### Settings with tabs `input.tab` splits the settings into Settings, Thresholds, About and Tips & FAQ. Every input carries its explanation as `description:`; the About and Tips tabs are `input.group` headings with `input.section(title:, description:)` rows, which the settings show as text. ## Adapting it - **Futures books.** Pick perpetual exchanges instead (`binancef`, `bybitf`, `okxf`) by changing the picker's `filter:` and default. - **A ratio pane.** Plot the ratio as a histogram on a pane of its own to see the imbalance over time. - **Alerts.** Trigger an alert when the reading crosses Lvl 3 on either side. - **A z-score pane.** Plot `z` as a histogram with lines at ±2 to see how close the book came to a band. ## Related - [Data and update events](/docs/scripting/guides/data-and-events#other-exchanges-and-symbols) describes how other markets are read and combined. - [Inputs and visuals](/docs/scripting/guides/inputs-and-visuals) covers exchange pickers and settings tabs. - [Liquidity profile](/docs/scripting/examples/liquidity-profile) reads the chart's own book by depth. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/aggregated-spot-bands --- # Liquidation magnets > Mark the price levels where liquidation cascades fired, keep them on the chart until price returns, and plot realised liquidations underneath. A liquidation cascade leaves a mark on the market: a burst of forced orders at one price, often the end of a move. Traders watch those levels afterwards, because price tends to come back and test where the leverage was flushed. This script watches the liquidations of each bar. When one side's liquidations are many times their recent average, it marks the bar's extreme as a level: long liquidations at the low (longs are liquidated as price falls), short liquidations at the high. Each level extends to the right with its size until price trades back into it; then the line stops at that bar. A pane shows the liquidations themselves, longs below zero and shorts above. ```flowscope title="liquidation-magnets.fs" script "Liquidation magnets" input ( avgLen = input.int(100, title: "Average length (bars)", min: 10, max: 1000) burstMult = input.float(4.0, title: "Burst (x average)", min: 1.5, max: 50.0) minUsd = input.float(1_000_000.0, title: "Smallest burst (USD)", min: 0.0) ) data ( chart = subscribe(data.ohlcv) stats = subscribe(data.stat) ) type Pocket { key: int price: float usd: float longs: bool start: time open: bool } type Burst { side: string price: float usd: float } state ( pockets = Pocket[](40) count = 0 lines = entities.linePool(max: 60) ) pane liqPane = pane(title: "Liquidations (USD)", height: 0.2) plot ( longLiqs = plot.histogram(title: "Longs liquidated", color: color.red, on: liqPane) shortLiqs = plot.histogram(title: "Shorts liquidated", color: color.green, on: liqPane) ) alert burstAlert = alert(title: "Liquidation burst", onClose: true, payload: Burst) fn label(usd: float, longs: bool) -> string { return str.format("{0} liqs {1:,.1}M", longs ? "Long" : "Short", usd / 1_000_000.0) } on chart.close { // A sell liquidates a long, a buy liquidates a short. let longUsd = stats.sellLiq ?? 0.0 let shortUsd = stats.buyLiq ?? 0.0 longLiqs.plot(0.0 - longUsd) shortLiqs.plot(shortUsd) let longAvg = ta.sma(longUsd, avgLen) let shortAvg = ta.sma(shortUsd, avgLen) let t = chart.time let h = chart.high let l = chart.low if t == null || h == null || l == null { return } // Close the levels price has traded back into. for i, p in pockets { if p.open && t > p.start && l <= p.price && h >= p.price { pockets[i] = Pocket { key: p.key, price: p.price, usd: p.usd, longs: p.longs, start: p.start, open: false } let tone = p.longs ? color.red : color.green lines.get(p.key).set(p.start, p.price, t, p.price, width: 1.0, color: color.withAlpha(tone, 120), style: linestyle.dotted, text: label(p.usd, p.longs)) } } let longBurst = longAvg != null && longUsd >= minUsd && longUsd > longAvg * burstMult let shortBurst = shortAvg != null && shortUsd >= minUsd && shortUsd > shortAvg * burstMult if longBurst || shortBurst { let longs = longBurst && longUsd >= shortUsd let usd = longs ? longUsd : shortUsd let price = longs ? l : h count += 1 if pockets.isFull { pockets.shift() } pockets.push(Pocket { key: count, price: price, usd: usd, longs: longs, start: t, open: true }) lines.get(count).set(t, price, t, price, width: 2.0, color: longs ? color.red : color.green, extend: extend.right, text: label(usd, longs)) burstAlert.trigger(Burst { side: longs ? "longs" : "shorts", price: price, usd: usd }) } } ``` ## How it works ### Liquidations per bar `data.stat` carries the market's liquidations: `sellLiq` is the value of liquidation orders that sold in the bar (long positions being closed), `buyLiq` the value that bought (shorts). On a chart they come from what the market recorded, the hub's history included; spot markets have none. The script treats a missing value as zero liquidations, which is what it means here. ### What counts as a burst A fixed USD threshold would fire all day on BTC and never on a small coin. The script compares each side with its own average over `avgLen` bars and also asks for a minimum size, so a quiet market does not turn a few thousand dollars into a signal. `ta.sma` is called on every bar, before any early `return`, so its history has no holes. ### Levels as records Each level is a `Pocket` record in a `Pocket[]` with room for 40. When it is full, `shift()` drops the oldest before the new one is pushed. Iterating an array gives copies, so a level that price has reached is written back by index with `open: false`. Its line keeps its pool key and is redrawn ending at the touching bar, dotted, so the chart shows where the level held until. ### Why the bar's extreme Long liquidations fire as price falls through the longs' liquidation prices, so the cascade's end is near the bar's low; for shorts it is near the high. With the bar's data alone that is the best estimate of where the forced orders hit. Use a shorter chart interval to place the level more precisely. ## Measured liquidation maps The levels here come from liquidations that already happened. For where open positions *would* be liquidated, read the liquidation maps with `data.liqmap`: the model of the chart's liquidation heatmap (from open-interest changes, by leverage tier) or Hyperliquid's maps measured from public positions. See [order book and liquidation maps](/docs/scripting/guides/heatmap-data) and the [Liquidation Sweep Reversal](/docs/scripting/examples/liquidation-sweep-reversal) strategy, which trades sweeps into those clusters. ## Adapting it - **Only the bigger side.** Drop `shortBurst` to track long flushes alone, the classic capitulation bottom. - **Weight by size.** Pass `width: math.clamp(usd / minUsd, 1.0, 4.0)` so larger bursts draw thicker lines. - **Expire old levels.** Close levels older than a day: compare `t - p.start` with `time.days(1)`. ## Related - [Funding extremes](/docs/scripting/examples/funding-extremes) for positioning stress from funding. - [User-defined types](/docs/scripting/guides/user-defined-types#copying) explains writing records back by index. - [`data` reference](/docs/scripting/reference/catalog/data), [`entities` reference](/docs/scripting/reference/catalog/entities) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/liquidation-magnets --- # Funding extremes > Turn funding into a z-score with extreme bands, tint the chart when positioning is crowded and get alerted when funding stretches. Funding tells you which side of the perpetual market is paying to stay in its position. Its absolute level is hard to judge: 0.01 % per period is normal on one coin and stretched on another, and what counts as "high" drifts with the market. A z-score fixes that by measuring each reading against its own recent history. This script plots the funding z-score as columns coloured by severity and draws two pairs of bands. When funding reaches the severe band, the price pane gets a light tint, and alerts tell you when longs or shorts become crowded. The legend shows the current rate annualised. ```flowscope title="funding-extremes.fs" script "Funding extremes" input ( zLen = input.int(270, title: "Z-score length (bars)", min: 20, max: 2000) hot = input.float(2.0, title: "Extreme band (z)", min: 0.5, max: 5.0) severe = input.float(3.0, title: "Severe band (z)", min: 1.0, max: 8.0) perDay = input.int(3, title: "Funding periods per day", min: 1, max: 24, description: "3 for 8-hour funding, 24 for hourly") ) data stats = subscribe(data.stat) type Crowding { side: string rate: float z: float } pane zPane = pane(title: "Funding z-score", height: 0.25) plot ( zCols = plot.histogram(title: "Z-score", on: zPane) hotUp = plot.line(title: "Extreme +", color: color.withAlpha(color.red, 120), style: linestyle.dashed, on: zPane) hotDown = plot.line(title: "Extreme -", color: color.withAlpha(color.green, 120), style: linestyle.dashed, on: zPane) severeUp = plot.line(title: "Severe +", color: color.red, on: zPane) severeDown = plot.line(title: "Severe -", color: color.green, on: zPane) crowdTint = plot.bg(title: "Severe funding") ) alert ( longsCrowded = alert(title: "Funding: longs crowded", onClose: true, payload: Crowding) shortsCrowded = alert(title: "Funding: shorts crowded", onClose: true, payload: Crowding) ) on stats.close { let r = stats.fundingRate let mean = ta.sma(r, zLen) let sd = ta.stdev(r, zLen) let z: float? = r != null && mean != null && sd != null && sd > 0.0 ? (r - mean) / sd : null let upCross = ta.crossover(z, hot) let downCross = ta.crossunder(z, 0.0 - hot) hotUp.plot(hot) hotDown.plot(0.0 - hot) severeUp.plot(severe) severeDown.plot(0.0 - severe) let shade = match z { null => color.gray >= severe => color.red >= hot => color.withAlpha(color.red, 150) <= 0.0 - severe => color.green <= 0.0 - hot => color.withAlpha(color.green, 150) _ => color.withAlpha(color.gray, 160) } zCols.plot(z, color: shade) if math.abs(z ?? 0.0) >= severe { crowdTint.plot(color: color.withAlpha((z ?? 0.0) > 0.0 ? color.red : color.green, 30)) } if r != null { let annual = r * float(perDay) * 365.0 * 100.0 script.setTitle(str.format("Funding extremes {0:.1}% p.a. z {1:.2}", annual, z ?? 0.0)) if z != null && upCross { longsCrowded.trigger(Crowding { side: "longs", rate: r, z: z }) } if z != null && downCross { shortsCrowded.trigger(Crowding { side: "shorts", rate: r, z: z }) } } } ``` ## How it works ### Funding on a chart `data.stat` carries `fundingRate`: on past bars the last settled rate, on the newest bar the current one. The script needs no candles, so it subscribes only to statistics. Spot markets have no funding; there every value is `null` and nothing is drawn. ### The z-score The z-score measures how many standard deviations the current rate is from its mean over `zLen` bars. Because the window is in bars, its span depends on the chart timeframe: 270 bars on a 1h chart are a little over eleven days. The `float?` annotation types the `null` branch for the warm-up bars. All `ta.*` calls, including the two crossings, run at the top of the handler on every bar. Calling a `ta` function inside an `if` would skip bars and break its history. ### Colour by severity The `match` expression maps the z-score to a colour. Relational arms are tested in order, so the severe arms come before the extreme arms; the bands are inputs, which arms may compare with. A `null` arm handles the warm-up period, and `_` covers everything in between. ### Annualised rate Venues settle funding at different intervals: every eight hours on most, every hour on some. The rate is per period, so `perDay` turns it into a yearly percentage for the legend. Set it to the venue's schedule. ### Alerts with a payload Each alert carries a `Crowding` record with the side, the raw rate and the z-score. `ta.crossover(z, hot)` is true only on the bar where the z-score enters the band, so the alert fires once per excursion instead of on every bar that stays above it. `onClose: true` makes it fire on confirmed bars only. ## Variations - **Funding versus price.** Combine with [OI regimes](/docs/scripting/examples/oi-regimes): severe positive funding during a "new longs" regime is a crowded trend, during "short covering" it is late chasing. - **Absolute bands instead of z.** Plot the annualised rate directly and set bands at fixed yields such as 30 % and 60 % per year. - **Liquidation follow-through.** `stats.sellLiq` on the same subscription shows whether long liquidations spike in the bars after a severe positive reading. ## Related - [Liquidation magnets](/docs/scripting/examples/liquidation-magnets) - [Pattern matching](/docs/scripting/guides/pattern-matching), [inputs and visuals](/docs/scripting/guides/inputs-and-visuals) - [`data` reference](/docs/scripting/reference/catalog/data), [`alert` reference](/docs/scripting/reference/catalog/alert) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/funding-extremes --- # Size-band delta > Split each bar's delta into retail, mid-size and whale flow from the trade stream, and flag stretches where whales and retail trade against each other. Not every buyer is the same. A thousand small market buys and one block of the same size say different things about who is moving the market. This script classifies every print by its notional and keeps a separate delta for small, mid-size and large traders. You pick where retail ends and where whale size begins. One pane shows the whale delta as columns with the retail delta as a line on top. A second pane shows cumulative delta per group, restarting every day. When whales have been net buyers over the last few bars while retail sold (or the reverse), a marker appears on the price chart. ```flowscope title="size-band-delta.fs" script "Size-band delta" input ( retailUsd = input.float(10_000.0, title: "Retail below (USD)", min: 100.0) whaleUsd = input.float(250_000.0, title: "Whales from (USD)", min: 1_000.0) lookback = input.int(20, title: "Comparison window (bars)", min: 3, max: 200) minWhaleUsd = input.float(2_000_000.0, title: "Min whale delta in window (USD)", min: 0.0) ) data ( chart = subscribe(data.ohlcv) tape = subscribe(data.trades) ) pane ( barPane = pane(title: "Delta by size (USD)", height: 0.2) cumPane = pane(title: "Cumulative delta by size, daily", height: 0.2) ) plot ( whaleBars = plot.histogram(title: "Whale delta", on: barPane) retailLine = plot.line(title: "Retail delta", color: color.yellow, width: 2, on: barPane) whaleCum = plot.line(title: "Whales", color: color.cyan, width: 2, on: cumPane) midCum = plot.line(title: "Mid-size", color: color.gray, on: cumPane) retailCum = plot.line(title: "Retail", color: color.yellow, on: cumPane) whalesBuy = plot.marker(title: "Whales buy, retail sells", color: color.green, shape: shape.up) whalesSell = plot.marker(title: "Whales sell, retail buys", color: color.red, shape: shape.down) ) state ( retail = 0.0 mid = 0.0 whale = 0.0 retailDay = 0.0 midDay = 0.0 whaleDay = 0.0 whaleWindow = rolling(200) retailWindow = rolling(200) lastTime: time? = null ) // Signed notional of each print, sorted into its size group. on tape.trade(tr) { let usd = tr.price * tr.size let signed = tr.isBuy ? usd : 0.0 - usd if usd < retailUsd { retail += signed } else if usd < whaleUsd { mid += signed } else { whale += signed } } on chart.update { whaleBars.plot(whale, color: whale >= 0.0 ? color.green : color.red) retailLine.plot(retail) whaleCum.plot(whaleDay + whale) midCum.plot(midDay + mid) retailCum.plot(retailDay + retail) } on chart.close { // Restart the daily lines on the first bar of a new day. let t = chart.time ?? time.fromUnixSeconds(0) let newDay = time.isNewPeriod(t, lastTime, 1D) lastTime = t whaleDay = newDay ? whale : whaleDay + whale midDay = newDay ? mid : midDay + mid retailDay = newDay ? retail : retailDay + retail // Who was on which side over the last `lookback` bars. whaleWindow.push(whale) retailWindow.push(retail) let whaleSum = whaleWindow.sumLast(lookback) let retailSum = retailWindow.sumLast(lookback) let buyStretch = whaleSum >= minWhaleUsd && retailSum < 0.0 let sellStretch = whaleSum <= 0.0 - minWhaleUsd && retailSum > 0.0 whalesBuy.plot(buyStretch ? chart.low : null) whalesSell.plot(sellStretch ? chart.high : null) retail = 0.0 mid = 0.0 whale = 0.0 } ``` ## How it works ### Classify each print A print's notional is `tr.price * tr.size`; its sign comes from the aggressor, `tr.isBuy`. An `if` / `else if` chain sorts it into one of three groups. The thresholds are in USD, so the same settings mean the same trader size on every coin. ### Bars and days The trade handler only adds. `on chart.update` shows the running bar, `on chart.close` banks it: the daily sums restart when `time.isNewPeriod` sees a new day, and the per-bar sums go back to zero. The cumulative lines show the banked day plus the forming bar, so the live bar moves them too. ### Who is on the other side Two `rolling` windows keep each bar's whale and retail delta. `sumLast(lookback)` adds the newest `lookback` values. When whales bought at least `minWhaleUsd` over that window while retail was net short, the green marker appears under the bar; the red one marks the reverse. The windows hold 200 values, the input's maximum. ### History On a chart, trades reach back as far as the chart keeps them in memory; earlier bars read zero in every group. For a long history of total delta, use `data.vd` instead, as in [plots and panes](/docs/scripting/examples/plots-and-panes). ## Variations - **Count instead of size.** Add counters per group to separate one block from a burst of medium prints. - **Three-way markers.** Mark bars where mid-size traders lead both other groups. - **Feed a strategy.** Use the whale window sum as a filter in the [CVD breakout strategy](/docs/scripting/examples/cvd-breakout-strategy). ## Related - [Large prints](/docs/scripting/examples/large-prints) marks individual large trades on the chart. - [Data and update events](/docs/scripting/guides/data-and-events#trades) - [`collections` reference](/docs/scripting/reference/catalog/collections), [`time` reference](/docs/scripting/reference/catalog/time) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/size-band-delta --- # Swing levels > Turn confirmed swing highs and lows into support and resistance rays that run until price breaks them, then freeze as broken levels. Old swing highs and lows are where stops collect and where traders expect reactions. This script keeps them on the chart for you. Every confirmed swing high becomes a resistance ray and every swing low a support ray, starting at the swing and running to the right. When price closes through a level, the ray is replaced by a fixed segment that ends at the break, drawn faded and dotted so you can still see where the level was. Only a set number of active levels per side is kept, so the chart stays readable, and an alert tells you when a level breaks. ```flowscope title="swing-levels.fs" script "Swing levels" input ( left = input.int(10, title: "Swing bars left", min: 1, max: 50) right = input.int(5, title: "Swing bars right", min: 1, max: 50) maxLevels = input.int(8, title: "Active levels per side", min: 1, max: 40) useWicks = input.bool(false, title: "Break on wick instead of close") keepBroken = input.bool(true, title: "Keep broken levels") resColor = input.color(color.red, title: "Resistance") supColor = input.color(color.green, title: "Support") ) data chart = subscribe(data.ohlcv) type Level { id: int price: float start: time } type Break { kind: string level: float } state ( highs = Level[](maxLevels) lows = Level[](maxLevels) nextId = 0 lines = entities.linePool(max: 300) tags = entities.labelPool(max: 100) ) alert levelBreak = alert(title: "Swing level broken", onClose: true, payload: Break) // A broken level: a faded segment up to the break, or nothing. fn retire(lv: Level, t: time, tone: color) { tags.delete(lv.id) if keepBroken { lines.get(lv.id).set(lv.start, lv.price, t, lv.price, color: color.withAlpha(tone, 110), style: linestyle.dotted) } else { lines.delete(lv.id) } } // A new level: a ray from the swing to the right edge, with its price. fn draw(lv: Level, t: time, tone: color, prefix: string) { lines.get(lv.id).set(lv.start, lv.price, t, lv.price, width: 2.0, color: tone, extend: extend.right) tags.get(lv.id).set(lv.start, lv.price, str.format("{0} {1:.2}", prefix, lv.price), color: tone) } on chart.close { let ph = ta.pivotHigh(chart.high, left, right) let pl = ta.pivotLow(chart.low, left, right) let t = chart.time let swingTime = chart.time[right] let up = useWicks ? chart.high : chart.close let down = useWicks ? chart.low : chart.close if t != null && up != null && down != null { // 1. Retire resistance levels that price has broken. let keepHighs = Level[](maxLevels) for lv in highs { if up > lv.price { retire(lv, t, resColor) levelBreak.trigger(Break { kind: "resistance", level: lv.price }) } else { keepHighs.push(lv) } } highs = keepHighs // 2. The same for support. let keepLows = Level[](maxLevels) for lv in lows { if down < lv.price { retire(lv, t, supColor) levelBreak.trigger(Break { kind: "support", level: lv.price }) } else { keepLows.push(lv) } } lows = keepLows // 3. Add newly confirmed swings, dropping the oldest when full. if ph != null && swingTime != null { if highs.isFull { let old = highs.shift() if old != null { lines.delete(old.id) tags.delete(old.id) } } nextId += 1 let lv = Level { id: nextId, price: ph, start: swingTime } highs.push(lv) draw(lv, t, resColor, "R") } if pl != null && swingTime != null { if lows.isFull { let old = lows.shift() if old != null { lines.delete(old.id) tags.delete(old.id) } } nextId += 1 let lv = Level { id: nextId, price: pl, start: swingTime } lows.push(lv) draw(lv, t, supColor, "S") } } } ``` ## How it works ### Confirmed swings only `ta.pivotHigh(chart.high, left, right)` returns the swing price once the bar `right` bars ago is the highest of its neighbourhood. The swing's time is therefore read with the same offset, `chart.time[right]`, so the ray starts at the actual swing candle even though it is drawn `right` bars later. Both pivot calls sit at the top of the handler, outside the null check, so they see every bar. ### Rays for live levels, segments for history A line with `extend: extend.right` continues to the right edge of the chart. Drawing it once, from the swing to the current bar at the same price, is enough; it needs no update on later bars. When a level breaks, `retire` redraws the same pool entry as a faded, dotted segment from the swing to the break bar, or deletes it when broken levels are not kept. Each level's id is its key in both pools, so creation, update and removal always agree. ### Keeping the active list Active levels are stored as `Level` records in two arrays with capacity `maxLevels`. Removing elements from an array while looping over it is easy to get wrong, so the script walks the array, handles broken levels, and pushes survivors into a local array, which then replaces the state array: ```flowscope highs = keepHighs ``` New swings are appended at the end. When the array is full, `shift()` removes the oldest level, which is also the one furthest back in time, and its drawings are deleted with it. Pushing into a full array would stop the script, so the `isFull` check comes first. If several levels break on the same bar, the alert still fires only once for that bar, because an alert fires at most once per bar. ### Functions that draw `retire` and `draw` read the pools and inputs directly; functions see the script's state and setup values. Broken segments stay until the line pool's 300 entries are used up; then the oldest is recycled. ## Adapting it - **Higher-timeframe swings.** Subscribe to `timeframe: 4h`, run the pivot logic in `on htf.close`, and draw from the chart handler. See [market structure](/docs/scripting/examples/market-structure) for that pattern. - **Liquidity sweeps instead of breaks.** Require a wick through the level and a close back on the original side. That is a sweep of the stops above or below, not a break. - **Touch counter.** Add a `touches: int` field to `Level`, increment it when the bar's range reaches the level without breaking it, and show the count in the label. - **Volume filter.** Only keep swings whose bar had above-average volume, using `ta.sma(chart.volume, 20)` evaluated on every bar. ## Related - [CVD divergence](/docs/scripting/examples/cvd-divergence) compares the same swings against delta. - [User-defined types](/docs/scripting/guides/user-defined-types), [functions](/docs/scripting/guides/functions) - [`ta` reference](/docs/scripting/reference/catalog/ta), [`entities` reference](/docs/scripting/reference/catalog/entities) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/swing-levels --- # HTF trend filter > Take chart-timeframe EMA crosses only when the confirmed 1h and 4h trends agree, and show which signals the filter rejected. Fast crossovers on a 15-minute chart produce many signals, and most of the losing ones point against the larger trend. A higher-timeframe filter removes them: a long signal counts only if the hourly and four-hour trends are both up. This script computes trend state on two higher timeframes from their confirmed bars, combines it into an alignment value, and colours the chart's EMA pair by that alignment. A light background marks bars where both timeframes agree. Crosses in the direction of the alignment get a signal marker and an alert. Crosses against it get a small grey cross, so you can judge what the filter is keeping you out of. ```flowscope title="htf-trend-filter.fs" script "HTF trend filter" input ( midTf = input.timeframe(1h, title: "Middle timeframe") topTf = input.timeframe(4h, title: "Top timeframe") trendFast = input.int(20, title: "HTF fast EMA", min: 2, max: 200) trendSlow = input.int(50, title: "HTF slow EMA", min: 3, max: 400) sigFast = input.int(9, title: "Signal fast EMA", min: 2, max: 100) sigSlow = input.int(21, title: "Signal slow EMA", min: 3, max: 200) showRejected = input.bool(true, title: "Show rejected crosses") ) data ( chart = subscribe(data.ohlcv) mid = subscribe(data.ohlcv, timeframe: midTf) top = subscribe(data.ohlcv, timeframe: topTf) ) type SignalPayload { side: string price: float } state ( midTrend: int = 0 topTrend: int = 0 ) plot ( fast = plot.line(title: "Fast EMA", width: 2) slow = plot.line(title: "Slow EMA", color: color.gray) agree = plot.bg(title: "Trends agree") longSig = plot.marker(title: "Long", color: color.green, shape: shape.up, size: 5.0) shortSig = plot.marker(title: "Short", color: color.red, shape: shape.down, size: 5.0) rejected = plot.marker(title: "Rejected", color: color.gray, shape: shape.cross, size: 3.0) ) alert signal = alert(title: "Filtered EMA cross", onClose: true, payload: SignalPayload) // +1 up, -1 down, 0 undecided: EMAs stacked and price on the right side. fn trendOf(price: float?, f: float?, s: float?) -> int { if price != null && f != null && s != null { if f > s && price > s { return 1 } if f < s && price < s { return -1 } } return 0 } on mid.close { let f = ta.ema(mid.close, trendFast) let s = ta.ema(mid.close, trendSlow) midTrend = trendOf(mid.close, f, s) } on top.close { let f = ta.ema(top.close, trendFast) let s = ta.ema(top.close, trendSlow) topTrend = trendOf(top.close, f, s) } on chart.close { let c = chart.close let f = ta.ema(c, sigFast) let s = ta.ema(c, sigSlow) let crossUp = ta.crossover(f, s) let crossDown = ta.crossunder(f, s) let atr = chart.atr(14) let aligned = midTrend == topTrend ? midTrend : 0 let tone = match aligned { 1 => color.green -1 => color.red _ => color.gray } fast.plot(f, color: tone) slow.plot(s) if aligned != 0 { agree.plot(color: color.withAlpha(tone, 22)) } let goLong = crossUp && aligned == 1 let goShort = crossDown && aligned == -1 let offset = (atr ?? 0.0) * 0.5 longSig.plot(goLong ? chart.low - offset : null) shortSig.plot(goShort ? chart.high + offset : null) let skipped = showRejected && (crossUp || crossDown) && !goLong && !goShort rejected.plot(skipped ? c : null) if c != null && (goLong || goShort) { signal.trigger(SignalPayload { side: goLong ? "long" : "short", price: c }) } } ``` ## How it works ### Trend state per timeframe Each higher timeframe has its own subscription and its own `close` handler. Inside `on mid.close`, `mid.close` is the hourly bar that just confirmed, so `ta.ema(mid.close, trendFast)` advances once per hour, exactly like an EMA on an hourly chart. The result is reduced to a single integer in `state`: ```flowscope on mid.close { let f = ta.ema(mid.close, trendFast) let s = ta.ema(mid.close, trendSlow) midTrend = trendOf(mid.close, f, s) } ``` The alternative, calling `ta.ema(mid.close, ...)` from the chart's handler, would feed the same hourly value into the EMA four times per hour on a 15-minute chart and give a different, wrong result. Compute higher-timeframe indicators in the higher timeframe's own handler. ### Confirmed values only Because the trend state changes only when a higher-timeframe bar closes, the filter never looks at a forming hourly or four-hour candle. A signal that appears on the chart stays there; it cannot be revoked by the higher timeframe changing its mind later in the hour. [Multiple timeframes](/docs/scripting/guides/multiple-timeframes) and [repainting](/docs/scripting/guides/repainting) explain the rules behind this. On a chart, the hourly and four-hour bars are built from the chart's own bars, so both timeframes must be whole multiples of the chart's interval. The slow EMAs need `trendSlow` higher-timeframe bars to warm up; until then the trend is undecided and the left part of the chart shows no alignment. ### Alignment `aligned` is `+1` or `-1` only when both timeframes agree, and `0` otherwise. A `match` maps it to a colour for the fast EMA and the background. The signal rule is then one line per side: ```flowscope let goLong = crossUp && aligned == 1 ``` ### Rejected crosses Crosses that the filter blocks are plotted as small grey crosses on the close. Watching them for a while tells you whether the filter is too strict for the current market; if most rejected crosses would have worked, the higher-timeframe EMAs are probably too slow. ## Adapting it - **Any signal.** Replace the EMA cross with your own entry condition. The filter part, the two `close` handlers and `aligned`, stays the same. - **Different trend definitions.** Use the slope of a single EMA (`ta.rising(ema, 3)`) or the close against `ta.highest` / `ta.lowest` of the last N higher-timeframe bars. - **Allow partial agreement.** Accept a signal when the top timeframe agrees and the middle one is neutral: `topTrend == 1 && midTrend >= 0`. - **Make it a strategy.** Add a `strategy` declaration and call `strategy.entry` on `goLong` and `goShort`. See the [EMA and ATR strategy](/docs/scripting/examples/ema-atr-strategy). ## Related - [MTF dashboard](/docs/scripting/examples/mtf-dashboard) shows the same trend states in a table. - [Market structure](/docs/scripting/examples/market-structure) builds zones on a higher timeframe. - [`input` reference](/docs/scripting/reference/catalog/input), [`ta` reference](/docs/scripting/reference/catalog/ta) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/htf-trend-filter --- # Market structure > Build supply and demand zones from confirmed higher-timeframe swings, extend them on the chart, and react when the chart timeframe breaks out of them. Higher-timeframe swings carry more weight than the ones on your trading chart. A four-hour swing high marks a place where sellers took control for hours, and a close above it on your 15-minute chart is a break of structure worth knowing about. This script finds confirmed swing highs and lows on a higher timeframe and turns each into a zone: from the swing candle's extreme to its body. Supply zones sit above price, demand zones below. The zones extend bar by bar on the chart timeframe until a chart bar closes beyond them. At that point the zone is frozen and faded, a breakout marker appears, the script's structure bias flips, and an alert fires. ```flowscope title="market-structure.fs" script "Market structure" input ( zoneTf = input.timeframe(4h, title: "Zone timeframe") left = input.int(3, title: "Swing bars left", min: 1, max: 20) right = input.int(3, title: "Swing bars right", min: 1, max: 20) maxZones = input.int(6, title: "Zones per side", min: 1, max: 30) ) data ( chart = subscribe(data.ohlcv) htf = subscribe(data.ohlcv, timeframe: zoneTf) ) type Zone { id: int top: float bottom: float start: time } type BreakPayload { direction: string level: float } state ( supply = Zone[](maxZones) demand = Zone[](maxZones) nextId = 0 bias = 0 brokeUp = false brokeDown = false boxes = entities.boxPool(max: 200) ) plot ( upBreak = plot.marker(title: "Break up", color: color.green, shape: shape.up, size: 6.0) downBreak = plot.marker(title: "Break down", color: color.red, shape: shape.down, size: 6.0) ) alert structureBreak = alert(title: "Structure break", onClose: true, payload: BreakPayload) // Draw a zone up to `t`: solid while live, faded once broken. fn paint(z: Zone, t: time, tone: color, text: string, live: bool) { boxes.get(z.id).set(z.start, z.top, t, z.bottom, color: color.withAlpha(tone, live ? 45 : 15), borderWidth: 1.0, borderColor: color.withAlpha(tone, live ? 255 : 60), text: text) } // Zones come from confirmed swings on the zone timeframe only. on htf.close { let ph = ta.pivotHigh(htf.high, left, right) let pl = ta.pivotLow(htf.low, left, right) let t = htf.time[right] let o = htf.open[right] let c = htf.close[right] if t != null && o != null && c != null { if ph != null { if supply.isFull { let old = supply.shift() if old != null { boxes.delete(old.id) } } nextId += 1 supply.push(Zone { id: nextId, top: ph, bottom: math.max(o, c), start: t }) } if pl != null { if demand.isFull { let old = demand.shift() if old != null { boxes.delete(old.id) } } nextId += 1 demand.push(Zone { id: nextId, top: math.min(o, c), bottom: pl, start: t }) } } } // Extend live zones and detect breakouts on the chart timeframe. on chart.close { brokeUp = false brokeDown = false let c = chart.close let t = chart.time if c != null && t != null { let keepSupply = Zone[](maxZones) for z in supply { if c > z.top { brokeUp = true paint(z, t, color.red, "Broken supply", false) structureBreak.trigger(BreakPayload { direction: "up", level: z.top }) } else { paint(z, t, color.red, "Supply", true) keepSupply.push(z) } } supply = keepSupply let keepDemand = Zone[](maxZones) for z in demand { if c < z.bottom { brokeDown = true paint(z, t, color.green, "Broken demand", false) structureBreak.trigger(BreakPayload { direction: "down", level: z.bottom }) } else { paint(z, t, color.green, "Demand", true) keepDemand.push(z) } } demand = keepDemand } bias = brokeUp ? 1 : (brokeDown ? -1 : bias) upBreak.plot(brokeUp ? chart.low : null) downBreak.plot(brokeDown ? chart.high : null) let label = match bias { 1 => "bullish" -1 => "bearish" _ => "undecided" } script.setTitle(str.format("Market structure: {0}", label)) } ``` ## How it works ### Two handlers, two jobs The script splits its work by timeframe. `on htf.close` runs once per confirmed zone-timeframe bar and only creates zones. `on chart.close` runs on every chart bar and only extends zones and checks for breaks. The two communicate through the `supply` and `demand` arrays in `state`. Because zones come from `htf.close`, a zone appears only after its higher-timeframe swing is confirmed, `right` bars later on that timeframe. A zone never appears and then disappears because a forming four-hour candle changed. ### Zone geometry The swing candle is `right` bars back on the zone timeframe, so its time, open and close are read with that offset: ```flowscope let t = htf.time[right] let o = htf.open[right] let c = htf.close[right] ``` A supply zone runs from the swing high down to the top of the candle's body, `math.max(o, c)`. A demand zone runs from the swing low up to the bottom of the body. Wick-to-body zones are a common choice because the wick is where the rejection happened. ### Extending and freezing Each chart bar redraws every live zone with the same pool key, its id, and the current bar time as its right edge. Setting a pool entry again moves it instead of creating a new one. When a close breaks a zone, it is drawn one last time, faded, ending at the break bar, and is not pushed into the survivor list. From then on nothing touches it again, so it stays on the chart as history. The loop cost is small: at most `2 * maxZones` boxes per bar. Broken zones stay until the pool's 200 boxes are used up; then the oldest is recycled. ### Flags in state Locals are immutable, so the loop cannot set a local "broke up" flag. The script uses two `state` booleans instead and resets them at the start of each bar. After the loops they drive the markers and the structure bias, which keeps its previous value on bars without a break. ## Adapting it - **Retests.** Add a `touched: bool` field to `Zone` and trigger an alert the first time price trades back into a live zone without closing through it. - **Order-flow confirmation.** Require the breakout bar's delta, from `subscribe(data.vd)`, to point in the breakout direction before you count it. See [VD absorption](/docs/scripting/examples/vd-absorption). - **Zone strength.** Store the swing candle's volume in the zone and scale the fill opacity with `color.gradient`. - **Several timeframes.** Duplicate the HTF subscription and handler for `1D`, and tag zones from each timeframe with a different colour. ## Related - [Swing levels](/docs/scripting/examples/swing-levels) does the same on the chart timeframe with rays. - [Multiple timeframes](/docs/scripting/guides/multiple-timeframes), [user-defined types](/docs/scripting/guides/user-defined-types) - [`entities` reference](/docs/scripting/reference/catalog/entities), [`ta` reference](/docs/scripting/reference/catalog/ta) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/market-structure --- # Trading sessions > Shade the Asia, London and New York sessions in the background and box each session's high-low range as it develops. Crypto trades around the clock, but liquidity does not. Volume, volatility and the character of the tape change as Asia hands over to London and London to New York. Seeing the sessions on the chart helps you put a move in context: an Asia range that London breaks, a New York open that sweeps the London high. This script tints the background by session, with a blended tint where London and New York overlap, and draws a box around each session's range. The box grows with the session and stays on the chart once the session ends. Session hours are inputs, in UTC. ```flowscope title="trading-sessions.fs" script "Trading sessions" input ( asiaStart = input.int(0, title: "Asia from (UTC hour)", min: 0, max: 23) asiaEnd = input.int(8, title: "Asia to", min: 0, max: 24) londonStart = input.int(7, title: "London from", min: 0, max: 23) londonEnd = input.int(16, title: "London to", min: 0, max: 24) nyStart = input.int(13, title: "New York from", min: 0, max: 23) nyEnd = input.int(22, title: "New York to", min: 0, max: 24) showBg = input.bool(true, title: "Shade sessions") showBoxes = input.bool(true, title: "Box session ranges") asiaColor = input.color(color.purple, title: "Asia") londonColor = input.color(color.blue, title: "London") nyColor = input.color(color.amber, title: "New York") ) setup ( asia = time.sessionMinutes(asiaStart * 60, asiaEnd * 60) london = time.sessionMinutes(londonStart * 60, londonEnd * 60) ny = time.sessionMinutes(nyStart * 60, nyEnd * 60) ) data chart = subscribe(data.ohlcv) type Range { hi: float lo: float start: time } state ( asiaRange: Range? = null londonRange: Range? = null nyRange: Range? = null boxes = entities.boxPool(max: 300) ) plot shade = plot.bg(title: "Session") // The session's range including this bar, or null outside the session. fn track(r: Range?, start: time?, h: float, l: float) -> Range? { if start == null { return null } if r != null && r.start == start { return Range { hi: math.max(r.hi, h), lo: math.min(r.lo, l), start: start } } return Range { hi: h, lo: l, start: start } } fn paint(name: string, r: Range?, t: time, tone: color) { if showBoxes && r != null { boxes.get(name, r.start).set(r.start, r.hi, t + time.bars(1), r.lo, color: color.withAlpha(tone, 25), borderWidth: 1.0, borderColor: tone, text: name) } } on chart.close { let t = chart.time let h = chart.high let l = chart.low if t != null && h != null && l != null { asiaRange = track(asiaRange, asia.windowStart(t), h, l) londonRange = track(londonRange, london.windowStart(t), h, l) nyRange = track(nyRange, ny.windowStart(t), h, l) paint("Asia", asiaRange, t, asiaColor) paint("London", londonRange, t, londonColor) paint("New York", nyRange, t, nyColor) // New York wins over London, London over Asia; the overlap gets a blend. let inAsia = asia.contains(t) let inLondon = london.contains(t) let inNy = ny.contains(t) let tint = inLondon && inNy ? color.gradient(0.5, 0.0, 1.0, londonColor, nyColor) : inNy ? nyColor : inLondon ? londonColor : asiaColor if showBg && (inAsia || inLondon || inNy) { shade.plot(color: color.withAlpha(tint, 22)) } } } ``` ## How it works ### Sessions from inputs `time.sessionMinutes(start, end)` builds a daily UTC window from minutes after midnight; the hour inputs are multiplied by 60. Sessions are setup values, made once from the inputs. If the end is not after the start, the session wraps across midnight. `session.contains(t)` tells whether a bar's open time falls inside, and `session.windowStart(t)` returns when the current window began, or `null` outside it. ### A range record per session Each session's developing range is a nullable `Range` record in `state`. `track` takes the current record and returns the next one: `null` outside the session, a fresh range when the window start differs from the record's (a new day's session), otherwise the record widened by the bar. `r` is a parameter, so the `r != null` check narrows it for the rest of the condition and the block. ### One box per session and day The box's pool key has two parts, the session name and its window start, so every session of every day gets its own box. While a session runs, every bar sets the same key again, so the box grows. When the session ends, nothing sets that key any more, and the box stays where it is. The right edge is `t + time.bars(1)`, the end of the current bar, so the box covers the last candle completely. ### Background priority The background has a single colour per bar. A nested conditional picks it: the London and New York overlap gets the midpoint of both colours from `color.gradient`, otherwise the later session wins. The long expression breaks after the `:` operator, never before it. ### Time zones Sessions are UTC windows. Exchanges that follow daylight saving shift by an hour twice a year; adjust the hour inputs then. ## Adapting it - **Asia range breakout.** Keep the finished Asia range when `asia.exited(previousTime, t)` is true, and alert when a London bar closes outside it. - **Session VWAP.** Add `pv` and `vol` fields to `Range`, accumulate price times volume and volume in `track`, and plot `pv / vol` as a line that restarts with each session. - **Weekend shading.** `time.dayOfWeek(t)` is 6 on Saturday and 7 on Sunday, when liquidity is thinnest. ## Related - [Inputs and visuals](/docs/scripting/guides/inputs-and-visuals), [colors and gradients](/docs/scripting/guides/colors-and-gradients) - [`time` reference](/docs/scripting/reference/catalog/time), [`entities` reference](/docs/scripting/reference/catalog/entities) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/trading-sessions --- # Trend candles > Repaint the chart's candles by trend state, fade them when the trend is stretched thin, and mark every trend flip. Colouring candles by trend turns a judgement into something you see at a glance. This script draws its own candles on top of the price pane, using the same open, high, low and close as the chart, and colours them by a trend state built from two independent reads: a Supertrend-style ATR trailing line for direction and an EMA slope for momentum. When both agree, candles take the up or down colour. When they disagree, candles turn grey. The colour also gets stronger the further price is from its EMA, measured in ATRs, so you see when a trend is extended. A marker appears on the first bar of each new trend, and an optional line shows where the trend would flip. ```flowscope title="trend-candles.fs" script "Trend candles" input ( factor = input.float(3.0, title: "Trailing line (x ATR)", min: 0.5, max: 10.0) atrLen = input.int(10, title: "ATR length", min: 2, max: 100) emaLen = input.int(34, title: "EMA length", min: 2, max: 300) slopeBars = input.int(3, title: "Slope lookback (bars)", min: 1, max: 20) maxStretch = input.float(3.0, title: "Full colour at (x ATR from EMA)", min: 0.5, max: 10.0) showLine = input.bool(true, title: "Show trailing line") ) data chart = subscribe(data.ohlcv) type Flip { trend: string price: float } state ( lowerBand: float? = null upperBand: float? = null direction = 1 regime = 0 tone = color.gray ) plot ( candles = plot.candle(title: "Trend candles") trail = plot.line(title: "Trailing line", width: 2, style: linestyle.step) flipUp = plot.marker(title: "Uptrend starts", color: color.green, shape: shape.up, size: 5.0) flipDown = plot.marker(title: "Downtrend starts", color: color.red, shape: shape.down, size: 5.0) ) alert flip = alert(title: "Trend flip", onClose: true, payload: Flip) // The forming candle takes the colour of the last confirmed state. on chart.update { candles.plot(chart.open, chart.high, chart.low, chart.close, bodyColor: tone, wickColor: tone, borderColor: tone) } on chart.close { let h = chart.high let l = chart.low let c = chart.close let prevClose = chart.close[1] let atr = chart.atr(atrLen) let ema = ta.ema(c, emaLen) let slope = ta.change(ema, slopeBars) if h == null || l == null || c == null || atr == null { return } // An ATR band around the bar's middle that only ever tightens while the // close stays on its side; crossing the active band flips the direction. let mid = (h + l) / 2.0 let lb = lowerBand let ub = upperBand let lower = lb != null && prevClose != null && prevClose > lb ? math.max(mid - factor * atr, lb) : mid - factor * atr let upper = ub != null && prevClose != null && prevClose < ub ? math.min(mid + factor * atr, ub) : mid + factor * atr if direction == 1 && c < lower { direction = -1 } else if direction == -1 && c > upper { direction = 1 } lowerBand = lower upperBand = upper let line = direction == 1 ? lower : upper trail.plot(showLine ? line : null, color: direction == 1 ? color.green : color.red) // Direction from the trailing line, momentum from the EMA slope. let rising = (slope ?? 0.0) > 0.0 let falling = (slope ?? 0.0) < 0.0 let next = direction == 1 && rising ? 1 : direction == -1 && falling ? -1 : 0 // Stretch: distance from the EMA in ATRs, 0 .. maxStretch. let stretch = ema != null && atr > 0.0 ? math.abs(c - ema) / atr : 0.0 let base = match next { 1 => color.green -1 => color.red _ => color.gray } tone = color.gradient(stretch, 0.0, maxStretch, color.withAlpha(base, 110), base) candles.plot(chart.open, h, l, c, bodyColor: tone, wickColor: tone, borderColor: tone) // A flip is the first bar of a new non-neutral regime. let isFlipUp = next == 1 && regime != 1 let isFlipDown = next == -1 && regime != -1 regime = next flipUp.plot(isFlipUp ? l : null) flipDown.plot(isFlipDown ? h : null) if isFlipUp || isFlipDown { flip.trigger(Flip { trend: isFlipUp ? "up" : "down", price: c }) } } ``` ## How it works ### Your own candles `plot.candle` draws a full candle per bar from four values. Fed with the chart's own open, high, low and close, it sits exactly on top of the chart's candles. To see only the trend candles, hide the chart's own candles in the chart settings; the script does not need them. The trend state is decided on confirmed bars in `on chart.close`. `on chart.update` paints the forming candle in the colour of the last confirmed state, so a colour never changes after the fact; when the bar closes, the close handler paints it again with its own state. ### A trailing line from state The trailing line is what a Supertrend computes (`chart.supertrend(factor, len)` gives you the same line and its direction in one call; the example builds it by hand to show state at work): a band `factor` ATRs below the bar's middle while the trend is up, above it while the trend is down. The band only tightens while the previous close stays on its side, so it trails price. When a close crosses the active band, the direction flips. The two bands and the direction live in `state`, because each bar needs the previous bar's values; they are copied to locals (`lb`, `ub`) so null checks narrow them. ### Two reads, one state The EMA slope over `slopeBars` bars, from `ta.change`, adds momentum. The regime is `+1` only when both say up, `-1` only when both say down, and `0` in between. Grey candles are a reminder that the two disagree, often in a range or a turn. All `ta.*` calls happen at the top of the handler, before the early `return`, so each keeps a consistent history. ### Intensity from the stretch `color.gradient(value, bottom, top, start, end)` blends between two colours. The script blends from a translucent version of the trend colour at the EMA to the full colour at `maxStretch` ATRs away. Values outside the range are clamped, so anything beyond `maxStretch` gets the full colour. ### Flips A flip is the first bar of a new up or down regime: the regime of the previous bar is kept in `state` and compared. A move from up to neutral and back to up counts as a new flip; that is deliberate, because the neutral bars mean the two reads disagreed for a while. Because everything runs on confirmed bars, a marker never appears and disappears on the live bar; see [repainting](/docs/scripting/guides/repainting). ## Variations - **Order-flow trend.** Replace the EMA slope with the sign of a rolling sum of volume delta from `data.vd`, so candles turn green only when buyers are net aggressive. - **Higher-timeframe colouring.** Compute the state in `on htf.close` for a 1h subscription and colour 5-minute candles by the hourly trend. See [HTF trend filter](/docs/scripting/examples/htf-trend-filter). - **Bars instead of candles.** `plot.bar` takes the same four values and draws OHLC bars. - **Heikin-Ashi.** Compute Heikin-Ashi values in state and feed them to `plot.candle` in a separate pane. ## Related - [Colors and gradients](/docs/scripting/guides/colors-and-gradients) - [`plot` reference](/docs/scripting/reference/catalog/plot), [`color` reference](/docs/scripting/reference/catalog/color) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/trend-candles --- # Market stats table > Summarise price, volume, flow, open interest, funding and liquidations for the chart's market in a live table window. When you trade several coins, you want the essentials of each one in a single glance: how far price has moved today, whether buyers or sellers are in control, whether open interest is building, where funding stands and who has been liquidated. This script collects those numbers and shows them in a table window over the chart, with colour-coded values. The table has three columns: the measure, its value, and a short reading such as "buyers" or "building". Every number is formatted for reading, not for precision. ```flowscope title="market-stats-table.fs" script "Market stats" input ( lookback = input.int(24, title: "Window (bars)", min: 2, max: 500) hotFunding = input.float(30.0, title: "Funding hot above (% p.a.)", min: 1.0, max: 200.0) perDay = input.int(3, title: "Funding periods per day", min: 1, max: 24) ) data ( chart = subscribe(data.ohlcv) daily = subscribe(data.ohlcv, timeframe: 1D) oi = subscribe(data.oi) stats = subscribe(data.stat) book = subscribe(data.book) ) window dash = window(title: "Market stats") { table: table.new(columns: [table.col(title: "Measure", width: 1.4), table.col(title: "Value", align: align.right), table.col(title: "Reading")]), } state ( buys = rolling(lookback) sells = rolling(lookback) longLiqs = rolling(lookback) shortLiqs = rolling(lookback) ) // "+1.23%" for a value, "n/a" for none. fn signed(x: float?, suffix: string) -> string { if x == null { return "n/a" } return str.format("{0}{1:.2}{2}", x >= 0.0 ? "+" : "", x, suffix) } fn tone(x: float?) -> color { return (x ?? 0.0) >= 0.0 ? color.green : color.red } on chart.close { buys.push(chart.buyVolume) sells.push(chart.sellVolume) longLiqs.push(stats.sellLiq) shortLiqs.push(stats.buyLiq) } on chart.update { if chart.isLast { let c = chart.close let dayOpen = daily.forming.open let dayPct: float? = c != null && dayOpen != null && dayOpen != 0.0 ? (c - dayOpen) / dayOpen * 100.0 : null let b = buys.sum() let s = sells.sum() let buyShare = b + s > 0.0 ? b / (b + s) * 100.0 : 50.0 let oiNow = oi.close let oiThen = oi.close[lookback] let oiPct: float? = oiNow != null && oiThen != null && oiThen != 0.0 ? (oiNow - oiThen) / oiThen * 100.0 : null let rate = stats.forming.fundingRate let annual: float? = rate != null ? rate * float(perDay) * 365.0 * 100.0 : null let hot = math.abs(annual ?? 0.0) >= hotFunding let ll = longLiqs.sum() let sl = shortLiqs.sum() let pressure = book.imbalance(0.5) let t = dash.table t.row("price", str.format("{0} on {1}", context.base, context.exchange), str.format("{0}", c ?? 0.0), table.cell(signed(dayPct, "% today"), fg: tone(dayPct))) t.row("flow", str.format("Buy share, {0} bars", lookback), str.format("{0:.1}%", buyShare), table.cell(buyShare >= 50.0 ? "buyers" : "sellers", fg: buyShare >= 50.0 ? color.green : color.red)) t.row("oi", str.format("OI change, {0} bars", lookback), table.cell(signed(oiPct, "%"), fg: tone(oiPct)), (oiPct ?? 0.0) >= 0.0 ? "building" : "unwinding") t.row("funding", "Funding", table.cell(signed(annual, "% p.a."), fg: tone(annual)), table.cell(hot ? "crowded" : "normal", bg: hot ? color.withAlpha(color.amber, 60) : color.transparent)) t.row("liqs", str.format("Liquidations, {0} bars", lookback), str.format("L {0:,.0} / S {1:,.0}", ll, sl), table.cell(ll >= sl ? "longs flushed" : "shorts squeezed", fg: ll >= sl ? color.red : color.green)) t.row("book", "Book within 0.5%", table.cell(signed(pressure == null ? null : (pressure ?? 0.0) * 100.0, "%"), fg: tone(pressure)), (pressure ?? 0.0) >= 0.0 ? "bids heavier" : "asks heavier") } } ``` ## How it works ### A window with a table A `window` declares a floating window with named panels. Here it holds a single table made with `table.new` and three columns from `table.col`. The panel is addressed as `dash.table`, and the handler keeps a short local alias `t`. `row(key, cells...)` writes a whole row, one cell per column. A row with the same key is replaced, so each update rewrites the six rows in place. A cell is a plain value or `table.cell(value, fg:, bg:)` for colours. ### Collect on close, render at the live edge Rolling sums of buy and sell volume and of liquidations are collected in `on chart.close`, one value per confirmed bar, in `rolling` collections sized by the `lookback` input. The table itself is only useful for the present, so the update handler renders it only when `chart.isLast` is true. During history nothing is rendered, and at the live edge the table refreshes with every update. ### Reading other subscriptions All other sources are read from the chart's update handler. Each one gives its last confirmed period, except where the script explicitly asks for the developing one: `daily.forming.open` is today's open, while `daily.open` would be yesterday's, and `stats.forming.fundingRate` the current rate. `oi.close[lookback]` reaches back `lookback` periods on the OI subscription. See [data and update events](/docs/scripting/guides/data-and-events) for the full rules. `book.imbalance(0.5)` compares the resting value on both sides within 0.5 % of the mid price, from the book the chart has recorded. Before the recording reaches back far enough it is `null`, and the row reads "n/a". ### Formatting helpers Two small functions keep the cells consistent. `signed` adds a plus sign to positive numbers and returns `"n/a"` for missing values, so the table never shows a raw `null`. `tone` picks a colour by sign. `{0:,.0}` in a format string writes a number with thousands separators and no decimals. ## Adapting it - **Profile rows.** Add `profile = subscribe(data.volume)` and show the point of control of the last `lookback` bars from `profile.profile(lookback).poc()`. - **Large-print share.** Sum the notional of prints above a floor from a trades subscription, as in [large prints](/docs/scripting/examples/large-prints), and show its share of the volume. - **Heat colours.** Replace `tone` with `color.gradient` so the colour intensity follows the size of the move. ## Related - [MTF dashboard](/docs/scripting/examples/mtf-dashboard) uses the same table panel for several timeframes. - [Viewport HUD](/docs/scripting/examples/viewport-hud) shows a smaller summary directly on the chart. - [Custom axes and charts](/docs/scripting/guides/custom-axes-and-charts#tables), [`surfaces` reference](/docs/scripting/reference/catalog/surfaces) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/market-stats-table --- # MTF dashboard > Summarise trend, momentum and trend efficiency on three timeframes in one table window, with an overall alignment row. Before you take a trade on a short timeframe, you usually want to know what the larger ones are doing. Switching the chart back and forth costs time and breaks your focus. This script keeps three timeframes in one table: for each, the trend from an EMA pair, RSI, an efficiency ratio as a measure of trend strength, and how far price is from the slow EMA in ATRs. A final row tells you whether all three agree. Every value comes from confirmed bars of its own timeframe, so the table never flickers with a forming higher-timeframe candle. ```flowscope title="mtf-dashboard.fs" script "MTF dashboard" input ( tfLow = input.timeframe(15m, title: "Timeframe 1") tfMid = input.timeframe(1h, title: "Timeframe 2") tfHigh = input.timeframe(4h, title: "Timeframe 3") fastLen = input.int(21, title: "Fast EMA", min: 2, max: 200) slowLen = input.int(55, title: "Slow EMA", min: 3, max: 400) erLen = input.int(14, title: "Efficiency length", min: 2, max: 100) erTrend = input.float(0.3, title: "Trending above", min: 0.05, max: 1.0) ) data ( chart = subscribe(data.ohlcv) lower = subscribe(data.ohlcv, timeframe: tfLow) middle = subscribe(data.ohlcv, timeframe: tfMid) upper = subscribe(data.ohlcv, timeframe: tfHigh) ) type TfState { trend: int rsi: float? efficiency: float? stretch: float? } window dash = window(title: "MTF dashboard") { table: table.new(columns: [table.col(title: "Timeframe"), table.col(title: "Trend"), table.col(title: "RSI", align: align.right), table.col(title: "Efficiency", align: align.right), table.col(title: "From slow EMA", align: align.right)]), } state ( sLow = TfState { trend: 0, rsi: null, efficiency: null, stretch: null } sMid = TfState { trend: 0, rsi: null, efficiency: null, stretch: null } sHigh = TfState { trend: 0, rsi: null, efficiency: null, stretch: null } ) fn summarize(c: float?, fast: float?, slow: float?, rsi: float?, efficiency: float?, atr: float?) -> TfState { let up = c != null && fast != null && slow != null && fast > slow && c > slow let down = c != null && fast != null && slow != null && fast < slow && c < slow let stretch: float? = c != null && slow != null && atr != null && atr > 0.0 ? (c - slow) / atr : null return TfState { trend: up ? 1 : down ? -1 : 0, rsi: rsi, efficiency: efficiency, stretch: stretch } } // Net move over the summed bar-to-bar moves: 1 is a straight line, 0 noise. fn efficiencyOf(net: float?, path: float?) -> float? { if net == null || path == null || path <= 0.0 { return null } return math.abs(net) / path } fn trendText(t: int) -> string { return match t { 1 => "Up" -1 => "Down" _ => "Flat" } } fn trendColor(t: int) -> color { return match t { 1 => color.green -1 => color.red _ => color.gray } } fn writeRow(key: string, label: string, s: TfState) { let er = s.efficiency ?? 0.0 dash.table.row(key, label, table.cell(trendText(s.trend), fg: color.white, bg: color.withAlpha(trendColor(s.trend), 170)), table.cell(str.format("{0:.0}", s.rsi ?? 50.0), fg: color.gradient(s.rsi ?? 50.0, 30.0, 70.0, color.red, color.green)), table.cell(str.format("{0:.2}", er), fg: er >= erTrend ? color.white : color.gray), str.format("{0:.2} ATR", s.stretch ?? 0.0)) } on lower.close { let path = ta.sum(math.abs(ta.change(lower.close) ?? 0.0), erLen) sLow = summarize(lower.close, ta.ema(lower.close, fastLen), ta.ema(lower.close, slowLen), ta.rsi(lower.close, 14), efficiencyOf(ta.change(lower.close, erLen), path), lower.atr(14)) } on middle.close { let path = ta.sum(math.abs(ta.change(middle.close) ?? 0.0), erLen) sMid = summarize(middle.close, ta.ema(middle.close, fastLen), ta.ema(middle.close, slowLen), ta.rsi(middle.close, 14), efficiencyOf(ta.change(middle.close, erLen), path), middle.atr(14)) } on upper.close { let path = ta.sum(math.abs(ta.change(upper.close) ?? 0.0), erLen) sHigh = summarize(upper.close, ta.ema(upper.close, fastLen), ta.ema(upper.close, slowLen), ta.rsi(upper.close, 14), efficiencyOf(ta.change(upper.close, erLen), path), upper.atr(14)) } on chart.update { if chart.isLast { writeRow("1", str.format("{0}", tfLow), sLow) writeRow("2", str.format("{0}", tfMid), sMid) writeRow("3", str.format("{0}", tfHigh), sHigh) let sum = sLow.trend + sMid.trend + sHigh.trend let verdict = match sum { 3 => "All up" -3 => "All down" _ => "Mixed" } let verdictTone = sum == 3 ? color.green : sum == -3 ? color.red : color.gray dash.table.row("4", "Alignment", table.cell(verdict, fg: color.white, bg: color.withAlpha(verdictTone, 200)), "", "", "") script.setTitle(str.format("MTF dashboard: {0}", verdict)) } } ``` ## How it works ### One subscription and one handler per timeframe Each timeframe has its own OHLCV subscription and its own `close` handler. Inside `on middle.close`, every `ta.*` call sees one value per confirmed hourly bar, exactly as it would on an hourly chart. The handler condenses the results into a `TfState` record in `state`. `middle.atr(14)` is a method of the subscription, because it needs high, low and close. On a chart, the three timeframes are built from the chart's own bars, so each must be a whole multiple of the chart's interval: with the defaults, use the dashboard on a chart of 15 minutes or less. ### Efficiency instead of ADX The efficiency ratio divides the net move over `erLen` bars by the sum of the bar-to-bar moves. A market that moves in a straight line scores 1; one that chops back and forth scores near 0. `ta.sum` of the absolute changes is the path; `ta.change(close, erLen)` the net move. ### Pure helpers `summarize` turns raw indicator values into a trend code and a stretch, without touching state or drawing. `trendText` and `trendColor` map the code to text and colour with exhaustive `match` expressions. `writeRow` writes one table row; a long call may continue on the next lines after a comma. Because the three timeframes share all of this, adding a fourth is a matter of one subscription, one handler, one state value and one `writeRow` call. ### Rendering at the live edge The chart's update handler only renders when `chart.isLast` is true. It does not compute anything; it reads the three `TfState` values, which change only when their timeframe closes a bar. `str.format("{0}", tfLow)` writes a timeframe as its label, such as `15m`. ### Colour in cells `table.cell` takes a text colour and a background. The trend column uses a solid background, RSI a gradient from red at 30 to green at 70, and the efficiency is bright only above the trending threshold, so the table reads from a distance. ## Adapting it - **Order-flow columns.** Add `data.vd` subscriptions per timeframe and a column for the sign of the last confirmed bar's delta. - **OI on the top timeframe.** Subscribe to `data.oi` with `timeframe: tfHigh` and show the last bar's OI change next to the four-hour trend. - **Chart in the same window.** Add `price: chart.panel(title: "4h")` to the window and plot the slow EMA there with `on: dash.price`. See [custom axes and charts](/docs/scripting/guides/custom-axes-and-charts). - **Alert on alignment.** Trigger an alert from `on upper.close` when all three trends turn the same way. ## Related - [HTF trend filter](/docs/scripting/examples/htf-trend-filter) uses the same per-timeframe trend states as a signal filter. - [Market stats table](/docs/scripting/examples/market-stats-table) - [Multiple timeframes](/docs/scripting/guides/multiple-timeframes), [`surfaces` reference](/docs/scripting/reference/catalog/surfaces) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/mtf-dashboard --- # Viewport HUD > Pin a compact market card to a corner of the price pane that stays in place while you pan and zoom. Some information belongs on the chart but not on a price level: today's change, current volatility, how far price is from VWAP, where funding stands. Labels attached to a bar scroll away as soon as you pan. A heads-up display (HUD) does not: it is anchored to the viewport, so it stays in the same corner whatever part of the chart you look at. This script builds a small card with the market, price and daily change, ATR as a percentage of price, distance from the daily VWAP, funding, open-interest change and the active trading session. You choose the corner in the settings. ```flowscope title="viewport-hud.fs" script "Market HUD" input ( where = input.select("top right", title: "Corner", options: ["top left", "top right", "bottom left", "bottom right"]) oiBars = input.int(24, title: "OI change over (bars)", min: 1, max: 500) perDay = input.int(3, title: "Funding periods per day", min: 1, max: 24) showSession = input.bool(true, title: "Show session") ) data ( chart = subscribe(data.ohlcv) daily = subscribe(data.ohlcv, timeframe: 1D) oi = subscribe(data.oi) stats = subscribe(data.stat) ) setup ( corner = match where { "top left" => anchor.topLeft "bottom left" => anchor.bottomLeft "bottom right" => anchor.bottomRight _ => anchor.topRight } asia = time.session("00:00", "08:00", "Asia") london = time.session("07:00", "16:00", "London") newYork = time.session("13:00", "22:00", "New York") ) state ( card = entities.labelPool(max: 1, anchor: corner) previousTime: time? = null ) fn pct(x: float?, digits: int) -> string { if x == null { return "n/a" } let sign = x >= 0.0 ? "+" : "" return digits == 1 ? str.format("{0}{1:.1}%", sign, x) : str.format("{0}{1:.2}%", sign, x) } on chart.update { // Indicators run on every update so their history stays complete. let t = chart.time ?? time.fromUnixSeconds(0) let atr = chart.atr(14) let vwap = chart.vwap(reset: time.isNewPeriod(t, previousTime, 1D)) if chart.isNew { previousTime = t } if chart.isLast { let c = chart.close let dayOpen = daily.forming.open let dayPct: float? = c != null && dayOpen != null && dayOpen != 0.0 ? (c - dayOpen) / dayOpen * 100.0 : null let atrPct: float? = c != null && atr != null && c != 0.0 ? atr / c * 100.0 : null let vwapPct: float? = c != null && vwap != null && vwap != 0.0 ? (c - vwap) / vwap * 100.0 : null let oiNow = oi.close let oiThen = oi.close[oiBars] let oiPct: float? = oiNow != null && oiThen != null && oiThen != 0.0 ? (oiNow - oiThen) / oiThen * 100.0 : null let rate = stats.forming.fundingRate let annual: float? = rate != null ? rate * float(perDay) * 365.0 * 100.0 : null let session = newYork.contains(t) ? "New York" : london.contains(t) ? "London" : asia.contains(t) ? "Asia" : "Off hours" let lines = string[](8) lines.push(str.format("{0} {1} {2}", context.base, context.exchange, context.timeframe)) lines.push(str.format("Price {0} {1} today", c ?? 0.0, pct(dayPct, 2))) lines.push(str.format("ATR 14 {0:.2}% of price", atrPct ?? 0.0)) lines.push(str.format("VWAP {0} away", pct(vwapPct, 2))) lines.push(str.format("Funding {0} p.a.", pct(annual, 1))) lines.push(str.format("OI {0} bars {1}", oiBars, pct(oiPct, 2))) if showSession { lines.push(str.format("Session {0}", session)) } let text = lines.reduce("", |acc, line| acc == "" ? line : str.concat(acc, "\n", line)) card.get("market").set(12.0, 12.0, text, font: font.mono) } } ``` ## How it works ### Anchored to the viewport A drawing pool with an `anchor:` measures its coordinates in pixels from that point of the pane instead of in time and price. `card.get("market").set(12.0, 12.0, text)` places the label 12 pixels inward from the chosen corner, and the anchor also decides which corner of the label sits there. Panning and zooming do not move it. Setting the same key again replaces the text, which is how the card updates live. ### A corner chosen in settings The anchor constants cannot be offered directly in a dropdown, so the input is a plain text selection. A `match` expression in `setup` converts it into an anchor once, before the pool is made. The `_` arm makes the expression exhaustive and gives a sensible default. ### Indicators outside the live-edge check The card is only drawn when `chart.isLast` is true, at the live edge. The two indicators are computed before that check, on every update during history and live. If `chart.atr` and `chart.vwap` were only called at the live edge, they would have no history and return `null`. This rule applies to every function that keeps history: call it unconditionally, use the result conditionally. `chart.vwap(reset:)` restarts when `reset` is true, here on the first bar of each UTC day. The previous bar's time is stored once per bar, when `chart.isNew` says a bar has just opened. ### Building the text `string[](8)` creates a local array with room for eight lines, and `push` adds each line. `reduce` joins them with line breaks into one multi-line label, set in the monospace font so the columns line up. The `pct` helper formats a signed percentage and returns `"n/a"` when the value is missing, so the card never shows a raw `null`. `daily.forming.open` is today's open, because the daily subscription's forming bar is the day in progress. `oi.close[oiBars]` reads open interest `oiBars` confirmed periods back. Sessions are UTC windows from `time.session`. ## Adapting it - **Two cards.** Make a second pool with another anchor, for example an order-flow card with the last bar's liquidations from `stats.sellLiq` and `stats.buyLiq`, or the book imbalance from `data.book`. - **Warning lines.** Add a line only when something needs attention, such as funding above a threshold or price more than two ATRs from VWAP. - **Strategy status.** In a strategy script, show `strategy.positionSize`, `strategy.openProfit` and `strategy.netProfit` on the card. See [position and trades](/docs/scripting/strategies/position-and-trades). ## Related - [Market stats table](/docs/scripting/examples/market-stats-table) shows the same kind of data in a separate window. - [Inputs and visuals](/docs/scripting/guides/inputs-and-visuals) - [`entities` reference](/docs/scripting/reference/catalog/entities), [`ta` reference](/docs/scripting/reference/catalog/ta) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/viewport-hud --- # EMA and ATR strategy > A risk-sized EMA crossover strategy with an RSI filter, ATR-based stop and target, and a trailing stop. This strategy follows the trend in both directions. A crossover of a fast and a slow EMA proposes a trade, RSI confirms that momentum agrees, and the ATR at signal time sets the stop, the target and the trailing stop. Each trade risks a fixed share of equity, capped by the margin your leverage allows. It is a good template for your own ideas: the signal block is a few lines, while sizing, brackets, plots and the HUD can stay as they are. ## The script ```flowscope title="ema-atr-strategy.fs" script "EMA + ATR strategy" input ( fastLen = input.int(21, title: "Fast EMA", min: 2, max: 200) slowLen = input.int(55, title: "Slow EMA", min: 3, max: 400) rsiLen = input.int(14, title: "RSI length", min: 2, max: 50) rsiLong = input.float(52.0, title: "Min RSI for longs", min: 0.0, max: 100.0) rsiShort = input.float(48.0, title: "Max RSI for shorts", min: 0.0, max: 100.0) atrLen = input.int(14, title: "ATR length", min: 1, max: 100) stopAtr = input.float(1.5, title: "Stop (ATR)", min: 0.2) targetAtr = input.float(3.5, title: "Target (ATR)", min: 0.5) trailStartAtr = input.float(1.5, title: "Trail activates after (ATR)", min: 0.1) trailAtr = input.float(1.0, title: "Trail distance (ATR)", min: 0.1) riskPct = input.float(1.0, title: "Risk per trade (% equity)", min: 0.1, max: 5.0) lev = input.float(3.0, title: "Leverage", min: 1.0, max: 20.0) allowLongs = input.bool(true, title: "Allow longs") allowShorts = input.bool(true, title: "Allow shorts") ) strategy( initialCapital: 10000.0, commissionType: strategy.commission.percent, commissionValue: 0.04, slippage: 1, leverage: lev, fundingCosts: true ) data chart = subscribe(data.ohlcv) pane osc = pane(title: "RSI", height: 0.2) plot ( fastLine = plot.line(title: "Fast EMA", color: color.blue, width: 2) slowLine = plot.line(title: "Slow EMA", color: color.amber, width: 2) stopLine = plot.line(title: "Stop", color: color.red, style: linestyle.step) targetLine = plot.line(title: "Target", color: color.green, style: linestyle.step) rsiLine = plot.line(title: "RSI", color: color.purple, on: osc) rsiMid = plot.line(title: "50", color: color.gray, style: linestyle.dashed, on: osc) ) state ( pendingDir = 0.0 pendingAtr = 0.0 activeAtr: float? = null hud = entities.labelPool(max: 1, anchor: anchor.topRight) ) on chart.close { // 1. Indicators: always evaluated, every bar. let fast = ta.ema(chart.close, fastLen) let slow = ta.ema(chart.close, slowLen) let rsi = ta.rsi(chart.close, rsiLen) let atr = chart.atr(atrLen) let crossUp = ta.crossover(fast, slow) let crossDown = ta.crossunder(fast, slow) fastLine.plot(fast) slowLine.plot(slow) rsiLine.plot(rsi) rsiMid.plot(50.0) // 2. Promote the signal ATR once the matching position exists. let pos = strategy.positionSize if pendingDir != 0.0 && pos * pendingDir > 0.0 { activeAtr = pendingAtr pendingDir = 0.0 } if pos == 0.0 && pendingDir == 0.0 { activeAtr = null } // 3. Signals. let r = rsi ?? 50.0 let longSignal = allowLongs && crossUp && r >= rsiLong let shortSignal = allowShorts && crossDown && r <= rsiShort let price = chart.close if atr != null && price != null && atr > 0.0 { // 4. Size: risk budget, capped by 90 % of the margin leverage allows. let dist = atr * stopAtr let riskQty = strategy.equity * riskPct / 100.0 / dist let capQty = strategy.equity * 0.9 * lev / price let size = math.min(riskQty, capQty) // 5. Bracket distances in ticks, fixed at signal time. let lossTicks = math.round(dist / context.tickSize) let profitTicks = math.round(atr * targetAtr / context.tickSize) let trailOn = math.round(atr * trailStartAtr / context.tickSize) let trailOff = math.round(atr * trailAtr / context.tickSize) if longSignal && pos <= 0.0 { strategy.entry("Long", strategy.long, qty: size, comment: str.format("RSI {0:.0}", r)) strategy.exit("Long exit", "Long", loss: lossTicks, profit: profitTicks, trailPoints: trailOn, trailOffset: trailOff) pendingDir = 1.0 pendingAtr = atr } else if shortSignal && pos >= 0.0 { strategy.entry("Short", strategy.short, qty: size, comment: str.format("RSI {0:.0}", r)) strategy.exit("Short exit", "Short", loss: lossTicks, profit: profitTicks, trailPoints: trailOn, trailOffset: trailOff) pendingDir = -1.0 pendingAtr = atr } } // 6. Signal exits when no reversal was placed. if pos > 0.0 && crossDown && !shortSignal { strategy.close("Long", comment: "Cross down") } if pos < 0.0 && crossUp && !longSignal { strategy.close("Short", comment: "Cross up") } // 7. Stop and target rails for the open position. let avg = strategy.positionAvgPrice let eAtr = activeAtr if avg != null && eAtr != null && pos != 0.0 { let sign = pos > 0.0 ? 1.0 : -1.0 stopLine.plot(avg - sign * eAtr * stopAtr) targetLine.plot(avg + sign * eAtr * targetAtr) } // 8. HUD. let n = strategy.closedTrades let winRate = n > 0 ? float(strategy.winTrades) / float(n) * 100.0 : 0.0 let side = pos > 0.0 ? "LONG" : pos < 0.0 ? "SHORT" : "FLAT" let mode = chart.isRealtime ? "PAPER (live)" : "BACKTEST" let text = str.format("{0} {1}\nEquity {2:,.2}\nOpen P&L {3:,.2}\nTrades {4} Win {5:.1}%", mode, side, strategy.equity, strategy.openProfit, n, winRate) hud.get("status").set(12.0, 12.0, text, font: font.mono) } ``` Open it on a liquid perpetual such as BTCUSDT on Binance, 1h. The Strategy Tester opens at the bottom with the backtest; after the last historical bar the strategy continues as paper trading. ## How it works ### Entry rules A long needs three things on the same confirmed bar: the fast EMA crosses above the slow EMA, RSI is at or above `rsiLong`, and longs are allowed. Shorts mirror this with `rsiShort`. The defaults leave a small neutral zone between 48 and 52, so a cross in a flat, directionless market is skipped. All `ta.*` calls run at the top of the handler on every bar. Putting them behind an `if` would freeze their state on bars where the condition is false. `strategy.entry` reverses: a long signal while short closes the short and opens the long in one fill at the next bar's open. The `pos <= 0.0` and `pos >= 0.0` checks prevent re-sending the same entry while already in that direction. ### Position size `riskQty` is the quantity at which a stop-out costs `riskPct` of equity. `capQty` is the largest quantity that fits into 90 % of the margin available at the chosen leverage. The smaller one wins and is passed as `qty:` in contracts (base units). The cap keeps a very tight ATR from requesting an unaffordable position. ### Stop, target and trailing stop The bracket is defined in ticks, computed from the ATR **at signal time**: | Order | Distance from the fill | |---|---| | Stop-loss | `stopAtr` × ATR | | Take-profit | `targetAtr` × ATR | | Trailing stop | Activates after `trailStartAtr` × ATR of profit, then trails `trailAtr` × ATR behind the best price | All three belong to one bracket, so they are one-cancels-other: whichever fills first closes the trade and removes the others. With the defaults, a trade that moves 1.5 ATR in your favour can no longer lose more than about half an ATR, because the trail then sits 1 ATR behind the best price. ### Signal exits A cross against the position closes it at market even if neither stop nor target has been reached, unless the opposite signal already placed a reversal. Closing the entry cancels its bracket automatically. ### Plots and HUD The stop and target rails are drawn from `strategy.positionAvgPrice` and the ATR that was current when the trade was signalled. Because the entry fills on the bar after the signal, the script keeps that ATR as *pending* and only promotes it when the position's sign matches. This keeps the rails from jumping during a reversal. The HUD, a label anchored to the top-right corner, shows whether the strategy is backtesting or paper trading live, plus equity, open P&L, trade count and win rate. ### Costs The declaration charges 0.04 % per fill, one tick of slippage on market and stop fills, and real historical funding while a position is open. Check the Funding paid metric in the Strategy Tester: on higher timeframes with long holding times it can be a meaningful share of the result. ## Variations - **Long only.** Set *Allow shorts* off. On markets with a persistent uptrend this often improves the Long column and removes the drag of counter-trend shorts. - **No trailing stop.** Remove `trailPoints:` and `trailOffset:` to compare a pure stop/target bracket against the trailing version. - **Higher-timeframe filter.** Subscribe to a 4h series, compute its EMA in `on h4.close` into state, and only allow longs when the confirmed 4h close is above it. Never read `h4.forming` for the decision. - **Fill at the close.** Add `processOrdersOnClose: true` to the declaration to fill at the signal bar's close instead of the next open, and compare the results. - **Breakeven move.** Once open profit exceeds one ATR, call `strategy.exit("Long exit", "Long", stop: avg)` again to move the stop to the entry price. ## Related - [Strategies & backtesting](/docs/scripting/strategies), [orders](/docs/scripting/strategies/orders), [risk](/docs/scripting/strategies/risk) - [CVD breakout strategy](/docs/scripting/examples/cvd-breakout-strategy) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/ema-atr-strategy --- # CVD breakout strategy > A range breakout confirmed by cumulative delta and by the bar's buy/sell imbalance, with a funding filter, a trailing stop and a time stop. Price breakouts fail often. This strategy only takes a breakout when the order flow behind it agrees: cumulative volume delta must break out of its own range with price, and aggressive volume on the breakout bar must lean the same way. It skips entries into crowded funding and pays funding while positioned. It reads best on an aggregated market, where the flow covers every venue. ## The script ```flowscope title="cvd-breakout-strategy.fs" script "CVD breakout" input ( lookback = input.int(36, title: "Range lookback (bars)", min: 5, max: 500) minImbalance = input.float(0.15, title: "Min bar imbalance", min: 0.0, max: 1.0) maxFundingPct = input.float(0.03, title: "Max funding against entry (% per period)", min: 0.0) atrLen = input.int(14, title: "ATR length", min: 1, max: 100) trailAtr = input.float(2.0, title: "Trail distance (ATR)", min: 0.2) maxBars = input.int(48, title: "Time stop (bars)", min: 1, max: 1000) riskPct = input.float(0.75, title: "Risk per trade (% equity)", min: 0.1, max: 5.0) ) strategy( initialCapital: 25000.0, commissionType: strategy.commission.percent, commissionValue: 0.05, slippage: 2, leverage: 3.0, fundingCosts: true ) data ( chart = subscribe(data.ohlcv) cvd = subscribe(data.cvd) stats = subscribe(data.stat) ) pane flowPane = pane(title: "Bar imbalance", height: 0.2) plot ( hiLine = plot.line(title: "Range high", color: color.cyan, style: linestyle.step) loLine = plot.line(title: "Range low", color: color.cyan, style: linestyle.step) imbBars = plot.histogram(title: "Buy/sell imbalance", on: flowPane) ) state hud = entities.labelPool(max: 1, anchor: anchor.topRight) on chart.close { // Range of the previous bars (the current bar is excluded). let rangeHigh = ta.highest(chart.high[1], lookback) let rangeLow = ta.lowest(chart.low[1], lookback) let cvdHigh = ta.highest(cvd.close[1], lookback) let cvdLow = ta.lowest(cvd.close[1], lookback) let atr = chart.atr(atrLen) hiLine.plot(rangeHigh) loLine.plot(rangeLow) // Aggressive buys against sells on this bar: -1 .. +1. let buys = chart.buyVolume ?? 0.0 let sells = chart.sellVolume ?? 0.0 let imbalance = buys + sells > 0.0 ? (buys - sells) / (buys + sells) : 0.0 imbBars.plot(imbalance, color: imbalance >= 0.0 ? color.green : color.red) // Funding in percent per settlement period. let fundingPct = (stats.fundingRate ?? 0.0) * 100.0 let price = chart.close let cvdNow = cvd.close let priceUp = price != null && rangeHigh != null && price > rangeHigh let priceDown = price != null && rangeLow != null && price < rangeLow let cvdUp = cvdNow != null && cvdHigh != null && cvdNow > cvdHigh let cvdDown = cvdNow != null && cvdLow != null && cvdNow < cvdLow let longOk = priceUp && cvdUp && imbalance >= minImbalance && fundingPct <= maxFundingPct let shortOk = priceDown && cvdDown && imbalance <= 0.0 - minImbalance && fundingPct >= 0.0 - maxFundingPct let flat = strategy.positionSize == 0.0 if flat && atr != null && price != null && rangeHigh != null && rangeLow != null { let mid = (rangeHigh + rangeLow) / 2.0 let trailTicks = math.round(atr * trailAtr / context.tickSize) let riskCash = strategy.equity * riskPct / 100.0 if longOk && price > mid { // Stop at the middle of the broken range; risk sized to that distance. strategy.entry("BO long", strategy.long, qty: riskCash / (price - mid)) strategy.exit("BO long exit", "BO long", stop: mid, trailPoints: trailTicks, trailOffset: trailTicks) } else if shortOk && price < mid { strategy.entry("BO short", strategy.short, qty: riskCash / (mid - price)) strategy.exit("BO short exit", "BO short", stop: mid, trailPoints: trailTicks, trailOffset: trailTicks) } } // Time stop: breakouts that go nowhere are closed. let open = strategy.openTrade(0) if open != null && chart.index - open.entryBarIndex >= maxBars { strategy.closeAll(comment: "Time stop") } let text = str.format("Funding {0:.4}%\nImbalance {1:.2}\nEquity {2:,.2}\nNet {3:,.2} Trades {4}", fundingPct, imbalance, strategy.equity, strategy.netProfit, strategy.closedTrades) hud.get("status").set(12.0, 12.0, text, font: font.mono) } ``` Open a liquid market, for example BTC aggregated across venues, on a 15m or 1h chart, and add the script. The Strategy Tester opens with the backtest. ## How it works ### Two ranges, not one `rangeHigh` and `rangeLow` are the highest high and lowest low of the previous `lookback` bars. Passing `chart.high[1]` excludes the current bar, so a close above `rangeHigh` is a genuine breakout of a level that existed before the bar. The same is done for CVD: `cvd.close` must also exceed its own prior range. A price breakout with CVD still inside its range means passive sellers absorbed the move, which is the pattern this filter is designed to reject. ### Bar imbalance `buyVolume` and `sellVolume` are the bar's aggressive buy and sell volume. Their difference over their sum is an imbalance between −1 (all aggressive volume sold) and +1 (all bought). A long needs at least `minImbalance` on the breakout bar. The histogram in the lower pane shows the value on every bar, so you can see how often the filter passes. ### Funding filter `stats.fundingRate` is the funding rate per settlement period, as a fraction. The script converts it to percent and refuses longs when longs already pay more than `maxFundingPct`, and shorts when shorts pay that much. Crowded positioning makes breakouts more likely to reverse, and it also makes holding them expensive. Spot markets have no funding; the filter then always passes. ### Exits | Exit | Rule | |---|---| | Stop | The midpoint of the broken range, as an absolute price | | Trailing stop | Activates after `trailAtr` × ATR of profit, then trails by the same distance | | Time stop | `strategy.closeAll` after `maxBars` bars in the trade | There is no fixed target: breakouts that work tend to run, and the trailing stop lets them. Because the stop is a price level, the size is the risk budget divided by the distance from the signal close to that level. The real fill is at the next open, so realised risk can differ slightly. The time stop reads the open trade's `entryBarIndex` from `strategy.openTrade(0)`. ### Costs `fundingCosts: true` charges or credits funding at the market's settlements while a position is open. Commission is set to a conservative 0.05 % and slippage to 2 ticks. See the [broker emulator](/docs/scripting/strategies/broker-emulator) for how fills are simulated. ## Variations - **Large traders only.** Replace the bar imbalance with the imbalance of prints above a notional floor from a trades subscription, as in [size-band delta](/docs/scripting/examples/size-band-delta). Trades reach back only as far as the chart holds them, so the backtest gets shorter. - **Liquidation confirmation.** Require `stats.buyLiq` (shorts liquidated) above a threshold on long breakouts: forced buying adds fuel. - **Funding as a signal.** Invert the funding filter and only trade breakouts *against* crowded funding, then compare the Long and Short columns. - **Fixed target.** Add `profit:` in ticks to each `strategy.exit` (for example 3 × ATR) and compare the profit factor with the trailing-only version. ## Related - [EMA and ATR strategy](/docs/scripting/examples/ema-atr-strategy) - [Strategies & backtesting](/docs/scripting/strategies), [orders](/docs/scripting/strategies/orders) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/cvd-breakout-strategy --- # Liquidation sweep reversal > Fade a sweep into a dense cluster of the liquidation model once real liquidations spike, open interest falls and the delta turns, with the stop beyond the sweep. The liquidation heatmap is a model, not a list of orders: from open interest and taker flow it estimates where positions were opened, at which leverage, and so where they would be liquidated. Dense clusters of those levels act as magnets. Price runs into them, the forced orders fire in a burst, and once the burst is spent and the positions are gone the move often turns. This strategy waits for all of that to be visible in the data before it fades the sweep, and puts its stop beyond the sweep's extreme. It ships with Flowscope as a community script. It reads the cluster from [`data.liqmap`](/docs/scripting/guides/heatmap-data#liquidation-maps), the liquidations from `data.stat`, open interest from `data.oi` and the delta from the candles. ## The script ```flowscope title="liquidation-sweep-reversal.fs" script "Liquidation Sweep Reversal" // Fades a sweep into a dense liquidation cluster once the forced flow is // spent: price runs into the levels where the liquidation model expects // many positions to be liquidated (a "magnet"), liquidations really fire, // open interest falls (the positions are gone) and the delta turns. // // Long (a sweep down; shorts mirror it): // 1. Cluster: on the bar before, the model's long-liquidation levels // within the search range below the price hold a cell of at least // the set multiple of their average cell (and the smallest size). // 2. Sweep: this bar's low trades into the middle of that cell. // 3. Flush: the longs liquidated in the bar reach the set multiple of // their average and the smallest size; open interest falls by at // least the set share over the bar. // 4. Turn: the bar closes green with buying delta, or one of the next // bars does; new lows meanwhile move the sweep's low, unless price runs // on through the cluster (more than the set ATRs past it). // Entry at the next open; stop beyond the sweep low (a buffer of ATR) or // a fixed ATR distance; target a multiple of the risk or the nearest // short-liquidation cluster above; a time stop after the set bars. // // Everything is read from completed bars: the cluster from the map of the // bar before the sweep, the liquidations, open interest and delta of the // bar that just closed. Orders fill at the next bar's open. input ( input.tab(title: "Settings") input.section(title: "Market") marketSel = input.select("Chart market", title: "Market", options: ["Chart market", "Binance + Bybit + OKX", "Binance", "Bybit", "OKX"], description: "Whose liquidation map, liquidations, open interest and delta are read. Chart market (default): the chart's own. Binance + Bybit + OKX: the three modeled venues summed (the chart's coin there). The model covers Binance, Bybit and OKX futures; spot and other venues read nothing.") levFrom = input.select("5x", title: "Leverage from", options: ["5x", "10x", "25x", "50x", "75x", "100x"], description: "The lowest leverage tier the map counts. The model spreads every position it estimates over 5x, 10x, 25x, 50x, 75x and 100x; high tiers sit close to the entry, low tiers far from it.") levTo = input.select("100x", title: "Leverage to", options: ["5x", "10x", "25x", "50x", "75x", "100x"], sameLine: true, description: "The highest leverage tier the map counts.") input.section(title: "Cluster") searchPct = input.float(1.5, title: "Search range (%)", min: 0.1, max: 10.0, description: "How far from the price a cluster may sit, in percent of the price.") clusterMult = input.float(3.0, title: "Density (x average cell)", min: 1.0, max: 50.0, description: "A cluster's cell must hold at least this multiple of the average cell in the search range: a magnet, not the background.") minClusterUsd = input.float(250000.0, title: "Smallest cluster (USD)", min: 0.0, description: "And at least this notional. 0 turns the floor off.") input.section(title: "Flush and turn") liqLen = input.int(60, title: "Liquidation average (bars)", min: 5, max: 1000, description: "The bars the liquidations of the sweep are compared with.") liqMult = input.float(3.0, title: "Liquidation spike (x average)", min: 1.0, max: 100.0, description: "The liquidations of the swept side in the sweep bar against their average.") minLiqUsd = input.float(100000.0, title: "Smallest flush (USD)", min: 0.0, description: "And at least this much liquidated in the bar.") oiDropPct = input.float(0.05, title: "Open interest drop (%)", min: 0.0, max: 10.0, description: "Open interest must fall by at least this share over the sweep bar: positions closed, not opened. Bars without open interest give no signal.") confirmBars = input.int(5, title: "Bars for the turn", min: 1, max: 30, description: "How many bars after the sweep the delta may take to turn. 1: the sweep bar itself must close as the turn.") maxBeyond = input.float(1.0, title: "Run past the cluster (ATR)", min: 0.0, max: 10.0, description: "While waiting for the turn, price may push further (the stop follows the new extreme), but not more than this beyond the cluster: then the cascade goes on and the setup is dropped.") input.section(title: "Exits") stopMode = input.select("Beyond the sweep", title: "Stop", options: ["Beyond the sweep", "ATR"], description: "Beyond the sweep: below the sweep's low (above its high for shorts) by the buffer. ATR: a fixed distance from the entry.") stopBuffer = input.float(0.25, title: "Buffer beyond the sweep (ATR)", min: 0.0, max: 5.0) stopAtr = input.float(1.5, title: "ATR stop distance", min: 0.1, max: 20.0, sameLine: true) targetMode = input.select("R multiple", title: "Target", options: ["R multiple", "Opposite cluster"], description: "R multiple: the risk times the multiple. Opposite cluster: the nearest cluster of the other side within the target range (the next magnet), else the R multiple; never less than 1 R.") targetR = input.float(2.0, title: "Target (R)", min: 0.2, max: 20.0) targetPct = input.float(3.0, title: "Opposite cluster range (%)", min: 0.1, max: 20.0, sameLine: true) maxBars = input.int(60, title: "Time stop (bars)", min: 1, max: 5000, description: "A trade still open after this many bars is closed at market.") input.section(title: "Risk") riskPct = input.float(0.5, title: "Risk per trade (% equity)", min: 0.05, max: 10.0, description: "The size is set so that the stop loses this share of the equity.") maxExposure = input.float(3.0, title: "Largest position (x equity)", min: 0.1, max: 50.0, description: "A tight stop never makes the position larger than this multiple of the equity.") feePct = input.float(0.05, title: "Fee per side (%)", min: 0.0, max: 1.0, description: "Taker fee of each fill, for the Strategy Tester.") allowLong = input.bool(true, title: "Longs") allowShort = input.bool(true, title: "Shorts", sameLine: true) showLevels = input.bool(true, title: "Show clusters, stops and targets") longColor = input.color(#00e676, title: "Long") shortColor = input.color(#ff1744, title: "Short", sameLine: true) input.tab(title: "About") input.section(title: "The idea", description: "The liquidation heatmap is a model, not real orders: from open interest and taker flow it estimates where positions were opened and at which leverage, and so where they would be liquidated. Dense clusters of those levels act as magnets: price is drawn to them, and when it gets there the forced orders fire in a burst. Once the burst is over and open interest has dropped, the move has lost its fuel and often turns.") input.section(title: "What has to happen", description: "A cluster near the price on the bar before; the bar sweeping into it; real liquidations of that side spiking (data.stat) while open interest falls (data.oi); then the delta turning against the sweep. Only then a trade, against the sweep, with the stop beyond the sweep's extreme.") input.section(title: "Reading the chart", description: "The dotted lines are the nearest clusters below and above the price. A diamond marks a sweep that flushed; a label marks the entry with the liquidations and the drop of open interest. Entry, stop and target lines run while the trade is open.") input.section(title: "Data", description: "data.liqmap reads the same model as the Liquidation Heatmap, cell by cell, with the leverage tiers chosen here. Liquidations and open interest come from the market's recorded context, including the hub's history. Spot markets and venues without the model read nothing and give no signals.") input.section(title: "Limits", description: "Few sweeps qualify: on BTC one minute bars a handful a day at the defaults. The model is an estimate; Hyperliquid's maps (map: liqmap.hlLiquidations) are built from public positions instead. Backtests over short histories are anecdotes, not evidence.") ) setup ( venue = match marketSel { "Binance + Bybit + OKX" => markets.agg("binancef", "bybitf", "okxf") "Binance" => "binancef" "Bybit" => "bybitf" "OKX" => "okxf" _ => context.exchange } levMin = match levFrom { "10x" => 10.0 "25x" => 25.0 "50x" => 50.0 "75x" => 75.0 "100x" => 100.0 _ => 5.0 } levMax = match levTo { "5x" => 5.0 "10x" => 10.0 "25x" => 25.0 "50x" => 50.0 "75x" => 75.0 _ => 100.0 } ) strategy( initialCapital: 10000.0, commissionType: strategy.commission.percent, commissionValue: feePct, slippage: 2, leverage: 10.0, fundingCosts: true ) data ( chart = subscribe(data.ohlcv) flow = subscribe(data.ohlcv, exchange: venue) liq = subscribe(data.liqmap, exchange: venue, minLeverage: levMin, maxLeverage: levMax) stats = subscribe(data.stat, exchange: venue) oi = subscribe(data.oi, exchange: venue) ) plot ( belowLine = plot.line(title: "Cluster below", color: #ff9800a0, style: linestyle.dotted, showValue: false) aboveLine = plot.line(title: "Cluster above", color: #40c4ffa0, style: linestyle.dotted, showValue: false) ) type Setup { side: string price: float liquidated: float oiChangePct: float } state ( // The sweep waiting for its turn: 1 long, -1 short, 0 none. armed = 0 armedAt = 0 sweepExtreme = 0.0 clusterEdge = 0.0 sweepLiq = 0.0 sweepOi = 0.0 prevLongAvg: float? = null prevShortAvg: float? = null // The trade drawn. tradeDir = 0 entryPrice = 0.0 stopPrice = 0.0 targetPrice = 0.0 tradeFrom: time? = null nextId = 0 marks = entities.markerPool(max: 400) labels = entities.labelPool(max: 400) lines = entities.linePool(max: 1200) ) alert longAlert = alert(title: "Liquidation sweep long", onClose: true, payload: Setup) alert shortAlert = alert(title: "Liquidation sweep short", onClose: true, payload: Setup) // The smallest cluster notional within `pct` below (or above) the price of // the map `ago` bars back: the floor, and the density multiple of the // average cell there. fn clusterFloor(pct: float, up: bool, ago: int) -> float { let ref = liq.price(barsAgo: ago) ?? 0.0 let cell = liq.cellSize(barsAgo: ago) ?? 0.0 if ref <= 0.0 || cell <= 0.0 { return minClusterUsd } let total = up ? liq.densityAbove(pct, barsAgo: ago) ?? 0.0 : liq.densityBelow(pct, barsAgo: ago) ?? 0.0 let cells = math.max(1.0, ref * pct / 100.0 / cell) return math.max(minClusterUsd, clusterMult * total / cells) } // Draws the open trade's lines up to `t`. fn drawTrade(t: time) { if !showLevels || tradeFrom == null { return } let start = tradeFrom ?? t lines.get(nextId * 3).set(start, entryPrice, t, entryPrice, width: 1.0, color: #ffffffb0, style: linestyle.dashed, text: str.format("Entry {0:.2}", entryPrice)) lines.get(nextId * 3 + 1).set(start, stopPrice, t, stopPrice, width: 1.0, color: #ff5252, text: str.format("SL {0:.2}", stopPrice)) lines.get(nextId * 3 + 2).set(start, targetPrice, t, targetPrice, width: 1.0, color: #26a69a, text: str.format("TP {0:.2}", targetPrice)) } on chart.close { let o = chart.open let h = chart.high let l = chart.low let c = chart.close let t = chart.time // Stateful helpers on every bar. let atr = chart.atr(14) let longLiq = stats.sellLiq ?? 0.0 let shortLiq = stats.buyLiq ?? 0.0 let longAvgNow = ta.sma(longLiq, liqLen) let shortAvgNow = ta.sma(shortLiq, liqLen) let longAvg = prevLongAvg let shortAvg = prevShortAvg prevLongAvg = longAvgNow prevShortAvg = shortAvgNow if o == null || h == null || l == null || c == null || t == null || atr == null || atr <= 0.0 { return } // The magnets: the nearest dense clusters of the map just completed. let below = liq.clusterBelow(searchPct, minSize: clusterFloor(searchPct, false, 0)) let above = liq.clusterAbove(searchPct, minSize: clusterFloor(searchPct, true, 0)) if showLevels { belowLine.plot(below == null ? null : below.price + below.binSize / 2.0) aboveLine.plot(above == null ? null : above.price + above.binSize / 2.0) } // The trade on the chart ends with the position. let flat = strategy.positionSize == 0.0 if tradeDir != 0 { drawTrade(t) if flat { tradeDir = 0 } } // Time stop. let open = strategy.openTrade(0) if open != null && chart.index - open.entryBarIndex >= maxBars { strategy.closeAll(comment: "Time stop") } let delta = (flow.buyVolume ?? 0.0) - (flow.sellVolume ?? 0.0) let oiOpen = oi.open let oiClose = oi.close let oiChange = oiOpen != null && oiClose != null && oiOpen > 0.0 ? (oiClose / oiOpen - 1.0) * 100.0 : 0.0 let oiKnown = oiOpen != null && oiClose != null && oiOpen > 0.0 // A new sweep: into the cluster of the bar before, flushing that side. if flat && armed == 0 && oiKnown && oiChange <= 0.0 - oiDropPct { let prevBelow = liq.clusterBelow(searchPct, minSize: clusterFloor(searchPct, false, 1), barsAgo: 1) let prevAbove = liq.clusterAbove(searchPct, minSize: clusterFloor(searchPct, true, 1), barsAgo: 1) let longFlush = longAvg != null && longLiq >= minLiqUsd && longLiq >= liqMult * longAvg let shortFlush = shortAvg != null && shortLiq >= minLiqUsd && shortLiq >= liqMult * shortAvg if allowLong && prevBelow != null && longFlush && l <= prevBelow.price + prevBelow.binSize / 2.0 { armed = 1 armedAt = chart.index sweepExtreme = l clusterEdge = prevBelow.price sweepLiq = longLiq sweepOi = oiChange marks.get(chart.index).set(t, l - 0.2 * atr, shape: shape.diamond, color: longColor, size: 7.0) } else if allowShort && prevAbove != null && shortFlush && h >= prevAbove.price + prevAbove.binSize / 2.0 { armed = -1 armedAt = chart.index sweepExtreme = h clusterEdge = prevAbove.top sweepLiq = shortLiq sweepOi = oiChange marks.get(chart.index).set(t, h + 0.2 * atr, shape: shape.diamond, color: shortColor, size: 7.0) } } if !flat || armed == 0 { return } // Price pushing on: the sweep's extreme (and so the stop) follows it, // unless it ran through the cluster. No turn in time: the setup is over. let ranThrough = armed > 0 ? l < clusterEdge - maxBeyond * atr : h > clusterEdge + maxBeyond * atr if ranThrough || chart.index - armedAt >= confirmBars { armed = 0 return } sweepExtreme = armed > 0 ? math.min(sweepExtreme, l) : math.max(sweepExtreme, h) let turned = armed > 0 ? c > o && delta > 0.0 : c < o && delta < 0.0 if !turned { return } // The trade: sized by the risk to the stop, capped by the exposure. let dir = armed armed = 0 let sign = dir > 0 ? 1.0 : -1.0 let stop = stopMode == "ATR" ? c - sign * stopAtr * atr : sweepExtreme - sign * stopBuffer * atr let risk = (c - stop) * sign if risk <= 0.0 { return } let rTarget = c + sign * targetR * risk let target = rTarget if targetMode == "Opposite cluster" { let next = dir > 0 ? liq.clusterAbove(targetPct, minSize: clusterFloor(targetPct, true, 0)) : liq.clusterBelow(targetPct, minSize: clusterFloor(targetPct, false, 0)) if next != null { // Just before the magnet, at least one R away. let magnet = dir > 0 ? next.price : next.top target = (magnet - c) * sign >= risk ? magnet : rTarget } } let qty = math.min(strategy.equity * riskPct / 100.0 / risk, strategy.equity * maxExposure / c) if qty <= 0.0 { return } let name = dir > 0 ? "Sweep long" : "Sweep short" strategy.entry(name, dir > 0 ? strategy.long : strategy.short, qty: qty, comment: str.format("liq {0:,.0} · OI {1:.2}%", sweepLiq, sweepOi)) strategy.exit(name + " exit", name, stop: stop, limit: target, comment: "SL / TP") nextId += 1 tradeDir = dir entryPrice = c stopPrice = stop targetPrice = target tradeFrom = t drawTrade(t) let text = str.format("{0}\nliq {1:,.0}\nOI {2:.2}%", dir > 0 ? "Long" : "Short", sweepLiq, sweepOi) labels.get(nextId).set(t, dir > 0 ? l - 0.6 * atr : h + 0.6 * atr, text, color: dir > 0 ? longColor : shortColor) let payload = Setup { side: dir > 0 ? "long" : "short", price: c, liquidated: sweepLiq, oiChangePct: sweepOi } if dir > 0 { longAlert.trigger(payload) } else { shortAlert.trigger(payload) } } on chart.update { let status = strategy.positionSize > 0.0 ? " · long" : strategy.positionSize < 0.0 ? " · short" : armed != 0 ? " · swept, waiting for the turn" : "" script.setTitle(str.format("Liquidation Sweep Reversal{0}", status)) } ``` Open a Binance, Bybit or OKX perpetual (or their aggregate), 1m to 15m, and add the script. The Strategy Tester opens with the backtest. ## How it works ### The cluster comes from the bar before `liq.clusterBelow(searchPct, minSize: …, barsAgo: 1)` finds the nearest heatmap cell of long liquidations below the price as the map stood on the bar *before* the sweep. Reading the bar of the sweep itself would see the cluster already half gone (the map of a bar is its time-weighted mean, and swept levels leave it). The cell has to hold `clusterMult` times the average cell within the search range and at least `minClusterUsd`: a magnet, not the background. `clusterFloor` computes that threshold from `liq.densityBelow` and `liq.cellSize`. ### What makes it a flush The bar must trade into the middle of that cell, and three things have to show in the same bar: | Check | Read | Default | |---|---|---| | Real liquidations of the swept side spike | `stats.sellLiq` (longs) against its 60-bar average | 3× and at least $100,000 | | Positions closed, not opened | `oi.close / oi.open - 1` | at most −0.05 % | | The sweep reached the cluster | the bar's low against the cell's middle | — | Bars without open interest give no signal: missing data is not a drop. ### The turn The setup waits up to `confirmBars` bars for the delta to turn: a green bar with more taker buying than selling (shorts: a red bar with net selling). If price pushes further meanwhile, the sweep's extreme moves with it, and with it the stop; if it runs more than `maxBeyond` ATRs through the cluster, the cascade is still going and the setup is dropped. ### Exits and size | Exit | Rule | |---|---| | Stop | Beyond the sweep's extreme by `stopBuffer` ATR, or `stopAtr` ATR from the entry | | Target | `targetR` times the risk, or the nearest cluster on the other side within `targetPct` (at least 1 R) | | Time stop | `strategy.closeAll` after `maxBars` bars | The size risks `riskPct` of the equity to the stop, capped at `maxExposure` times the equity. Commission defaults to 0.05 % a side, slippage to 2 ticks, and funding is charged while a position is open. ### Leverage tiers `levFrom` and `levTo` keep only some tiers of the model (`minLeverage:` and `maxLeverage:` of the subscription). 50x to 100x keeps the clusters close to the entries, the ones a quick sweep reaches; 5x to 25x the deeper ones. See [leverage tiers](/docs/scripting/guides/heatmap-data#leverage-tiers) for how the split is made. ### No look-ahead Every value comes from bars that have closed: the cluster from the bar before the sweep, the liquidations, open interest and delta from the bar that just closed. Orders fill at the next bar's open. A backtest cut short at any bar trades exactly as the full one up to that bar (the test suite checks it). ## What we measured On the BTCUSDT Binance perpetual with the data hub's history from Thursday 15 October 14:52 to Friday 16 October 19:15 UTC (about 28 hours, all the history there was), at the defaults: | Chart | Trades | Result | |---|---|---| | 1m | 1 long (16 Oct 15:09, out at the target two minutes later) | +$49.67 on $10,000 (+0.50 %), commission $30.04 | | 5m | 1 short (16 Oct 11:35, after $7.1M of shorts were liquidated) | +$79.89 (+0.80 %) | Three sweeps flushed into a cluster on the 1m chart in those hours; one turned in time. One or two trades are an anecdote, not evidence: test it on the longest history you have before trusting any number. ## Adapting it - **Aggregate.** `Market: Binance + Bybit + OKX` sums the three modeled venues: their clusters, liquidations and open interest. - **Tighter clusters.** Raise `clusterMult` to 5 to trade only the densest magnets. - **Hyperliquid.** Subscribe to `liqmap.hlLiquidations` instead of the model for the liquidation prices of Hyperliquid's public positions. - **Confirm with the book.** Require a bid wall under the sweep with [`book.wallBelow`](/docs/scripting/guides/heatmap-data), as in [wall absorption](/docs/scripting/examples/wall-absorption). ## Related - [Order book and liquidation maps](/docs/scripting/guides/heatmap-data) - [Liquidation magnets](/docs/scripting/examples/liquidation-magnets), the realised side - [Strategies & backtesting](/docs/scripting/strategies) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/liquidation-sweep-reversal --- # Wall absorption > Trade a wall of the order book that market orders hit without getting through, with the stop behind it; mark walls that were pulled or filled. The liquidity heatmap shows real resting orders. A large one, a wall, is a price where someone is ready to trade size. When aggressive market orders run into it and it stays, refilled or simply big enough, the move runs out at that price: the wall absorbed it. That is a trade with a clear reason and a clear invalidation. Go with the wall; if price gets through it, the reason is gone, so the stop sits just behind it. Walls can also be pulled before price arrives (spoofing) or eaten through (a real breakout). The strategy tells those apart by comparing the wall's size before and after the bar with the volume that traded at its prices, and marks them on the chart; neither is traded. It ships with Flowscope as a community script. It reads the book from `data.book` (the heatmap's cells) and the volume at each price from `data.volume` (the footprints). ## The script ```flowscope title="wall-absorption.fs" script "Wall Absorption" // Trades a large resting order that takes the hit and holds: aggressive // market orders run into a wall of the order book, the wall stays (or // refills) and price fails to get through. The trade goes with the wall, // the stop sits behind it. // // Long at a bid wall (shorts mirror it at an ask wall): // 1. Wall: on the bar before, the nearest heatmap cell of the book below // the price within the search range that holds at least the set // multiple of the average cell there and the smallest notional. // 2. Hit: this bar trades into the wall's cell, and the market sells // traded at the wall's prices (the footprint) reach the set share of // what rested there. // 3. Held: the wall still holds at least the set share of its size after // the bar (absorbed, maybe refilled), and the bar closes not below the // wall, away from its low by the set share of its range. // Entry at the next open; stop behind the wall by a buffer of ATR; // target a multiple of the risk or the largest wall on the other side. // // What a wall did is told apart by the size it lost and the volume traded // at its price: lost with little traded there, it was pulled (a fake, often // spoofing); lost to trades, it was filled (a real breakout, no trade // against it). Both are marked when price was at the wall. // // The book of a bar is the heatmap's cell value: the mean resting size at // the bar's minute closes. Everything is read from the bars completed; // orders fill at the next bar's open. input ( input.tab(title: "Settings") input.section(title: "Market") marketSel = input.select("Chart market", title: "Market", options: ["Chart market", "Binance + Bybit + OKX", "Binance", "Bybit", "OKX", "Binance spot + Coinbase"], description: "Whose order book and footprint are read. Chart market (default): the chart's own. The other choices sum the books (and the volume traded at each price) of those venues for the chart's coin.") input.section(title: "Wall") searchPct = input.float(0.5, title: "Search range (%)", min: 0.05, max: 5.0, description: "How far from the mid a wall may rest, in percent of the price.") wallMult = input.float(4.0, title: "Size (x average cell)", min: 1.0, max: 100.0, description: "The wall's cell must hold at least this multiple of the average cell in the search range.") minWallUsd = input.float(500000.0, title: "Smallest wall (USD)", min: 0.0, description: "And at least this much resting, in the quote currency. 0 turns the floor off.") input.section(title: "Absorption") hitPct = input.float(20.0, title: "Traded into the wall (% of its size)", min: 0.0, max: 1000.0, description: "Market orders traded at the wall's prices in the bar (sells at a bid wall, buys at an ask wall), against the size that rested there before.") holdPct = input.float(60.0, title: "Wall left after the bar (%)", min: 0.0, max: 500.0, description: "The wall must still hold at least this share of its size: it absorbed the orders instead of being eaten or pulled. Above 100 % it refilled.") rejectPct = input.float(30.0, title: "Close away from the wall (% of the range)", min: 0.0, max: 100.0, description: "The bar must close at least this far from its extreme on the wall's side, and not beyond the wall.") goneShare = input.float(50.0, title: "Pulled or filled (% lost)", min: 1.0, max: 100.0, description: "A wall that lost at least this share is marked pulled (little traded there) or filled (traded away).") input.section(title: "Exits") stopBuffer = input.float(0.3, title: "Stop behind the wall (ATR)", min: 0.0, max: 10.0, description: "The stop sits this far beyond the far edge of the wall's cell.") targetMode = input.select("R multiple", title: "Target", options: ["R multiple", "Opposite wall"], description: "R multiple: the risk times the multiple. Opposite wall: the largest wall on the other side within the target range, else the R multiple; never less than 1 R.") targetR = input.float(2.0, title: "Target (R)", min: 0.2, max: 20.0) targetPct = input.float(1.0, title: "Opposite wall range (%)", min: 0.05, max: 10.0, sameLine: true) maxBars = input.int(45, title: "Time stop (bars)", min: 1, max: 5000, description: "A trade still open after this many bars is closed at market.") input.section(title: "Risk") riskPct = input.float(0.5, title: "Risk per trade (% equity)", min: 0.05, max: 10.0, description: "The size is set so that the stop loses this share of the equity.") maxExposure = input.float(3.0, title: "Largest position (x equity)", min: 0.1, max: 50.0, description: "A tight stop never makes the position larger than this multiple of the equity.") feePct = input.float(0.05, title: "Fee per side (%)", min: 0.0, max: 1.0, description: "Taker fee of each fill, for the Strategy Tester.") allowLong = input.bool(true, title: "Longs") allowShort = input.bool(true, title: "Shorts", sameLine: true) showWalls = input.bool(true, title: "Show walls, events, stops and targets") longColor = input.color(#00e676, title: "Long") shortColor = input.color(#ff1744, title: "Short", sameLine: true) input.tab(title: "About") input.section(title: "The idea", description: "The liquidity heatmap shows real resting orders. A wall that market orders hit without getting through tells that someone is absorbing them: the move runs out at that price and often turns. The trade follows the wall, with the stop just behind it: if the wall goes, the reason for the trade goes with it.") input.section(title: "Fakes and breakouts", description: "Walls can be pulled before price arrives (spoofing) or eaten through (a real breakout). The script compares the wall's size before and after the bar with the volume that traded at its prices: a wall that shrank with little traded was pulled, one that shrank as much as traded was filled. Neither gives a trade; both are marked.") input.section(title: "Reading the chart", description: "The dotted lines mark the nearest walls below and above. A triangle marks an absorption, a label the entry with what traded into the wall and what was left. A cross marks a pulled wall, a square a filled one. Entry, stop and target lines run while the trade is open.") input.section(title: "Data", description: "data.book gives each bar's book as the heatmap's cells show it (the mean resting size at the bar's minute closes, summed in the heatmap's price bin); data.volume gives the volume traded at each price from the footprints. Book history reaches 2,000 bars back.") input.section(title: "Limits", description: "The book is sampled once a minute: a wall placed and pulled within a minute is not seen, and on bars of several minutes a wall that stood only part of the bar counts by that share. Backtests over short histories are anecdotes, not evidence.") ) setup venue = match marketSel { "Binance + Bybit + OKX" => markets.agg("binancef", "bybitf", "okxf") "Binance" => "binancef" "Bybit" => "bybitf" "OKX" => "okxf" "Binance spot + Coinbase" => markets.agg("binance", "coinbase") _ => context.exchange } strategy( initialCapital: 10000.0, commissionType: strategy.commission.percent, commissionValue: feePct, slippage: 2, leverage: 10.0, fundingCosts: true ) data ( chart = subscribe(data.ohlcv) book = subscribe(data.book, exchange: venue) vol = subscribe(data.volume, exchange: venue) ) plot ( bidWall = plot.line(title: "Bid wall", color: #00e67690, style: linestyle.dotted, showValue: false) askWall = plot.line(title: "Ask wall", color: #ff174490, style: linestyle.dotted, showValue: false) ) type Absorbed { side: string wall: float rested: float traded: float left: float } state ( tradeDir = 0 entryPrice = 0.0 stopPrice = 0.0 targetPrice = 0.0 tradeFrom: time? = null nextId = 0 marks = entities.markerPool(max: 600) labels = entities.labelPool(max: 400) lines = entities.linePool(max: 1200) ) alert longAlert = alert(title: "Wall absorbed, long", onClose: true, payload: Absorbed) alert shortAlert = alert(title: "Wall absorbed, short", onClose: true, payload: Absorbed) // The smallest wall (quote value) within `pct` of the mid `ago` bars back: // the floor, and the multiple of the average cell there. fn wallFloor(pct: float, ago: int) -> float { let mid = book.mid(barsAgo: ago) ?? 0.0 let cell = book.cellSize(barsAgo: ago) ?? 0.0 if mid <= 0.0 || cell <= 0.0 { return minWallUsd } let total = book.liquidity(mid * (1.0 - pct / 100.0), mid * (1.0 + pct / 100.0), unit: bookunit.quote, barsAgo: ago) ?? 0.0 let cells = math.max(1.0, 2.0 * mid * pct / 100.0 / cell) return math.max(minWallUsd, wallMult * total / cells) } // Sold (bought) by market orders at prices from `lo` to `hi` in this bar, // in base units. fn tradedAt(lo: float, hi: float) -> float { let s = vol.summary(from: lo, to: hi) return s == null ? 0.0 : s.sell } fn boughtAt(lo: float, hi: float) -> float { let s = vol.summary(from: lo, to: hi) return s == null ? 0.0 : s.buy } // Draws the open trade's lines up to `t`. fn drawTrade(t: time) { if !showWalls || tradeFrom == null { return } let start = tradeFrom ?? t lines.get(nextId * 3).set(start, entryPrice, t, entryPrice, width: 1.0, color: #ffffffb0, style: linestyle.dashed, text: str.format("Entry {0:.2}", entryPrice)) lines.get(nextId * 3 + 1).set(start, stopPrice, t, stopPrice, width: 1.0, color: #ff5252, text: str.format("SL {0:.2}", stopPrice)) lines.get(nextId * 3 + 2).set(start, targetPrice, t, targetPrice, width: 1.0, color: #26a69a, text: str.format("TP {0:.2}", targetPrice)) } on chart.close { let o = chart.open let h = chart.high let l = chart.low let c = chart.close let t = chart.time let atr = chart.atr(14) if o == null || h == null || l == null || c == null || t == null || atr == null || atr <= 0.0 { return } let range = h - l // The walls of the book just completed, for the chart. let floorNow = wallFloor(searchPct, 0) let below = book.wallBelow(searchPct, minSize: floorNow, nearest: true, unit: bookunit.quote) let above = book.wallAbove(searchPct, minSize: floorNow, nearest: true, unit: bookunit.quote) if showWalls { bidWall.plot(below == null ? null : below.price + below.binSize / 2.0) askWall.plot(above == null ? null : above.price + above.binSize / 2.0) } let flat = strategy.positionSize == 0.0 if tradeDir != 0 { drawTrade(t) if flat { tradeDir = 0 } } let open = strategy.openTrade(0) if open != null && chart.index - open.entryBarIndex >= maxBars { strategy.closeAll(comment: "Time stop") } // The walls that stood before this bar, and what the bar did to them. let floorBefore = wallFloor(searchPct, 1) let bidW = book.wallBelow(searchPct, minSize: floorBefore, nearest: true, unit: bookunit.quote, barsAgo: 1) let askW = book.wallAbove(searchPct, minSize: floorBefore, nearest: true, unit: bookunit.quote, barsAgo: 1) let gone = goneShare / 100.0 let signal = 0 let rested = 0.0 let traded = 0.0 let left = 0.0 let wallPrice = 0.0 let wallEdge = 0.0 if bidW != null && l <= bidW.top { // Rows of the footprint inside the wall's cell (not the next cell's; // the edges a hair low against rounding). let lo = bidW.price - bidW.binSize * 0.001 let hi = bidW.top - bidW.binSize * 0.001 let ch = book.change(bidW.price + bidW.binSize / 2.0) let sold = tradedAt(lo, hi) if ch != null && ch.before > 0.0 { let held = ch.after >= ch.before * holdPct / 100.0 let lost = ch.before - ch.after if allowLong && held && sold >= ch.before * hitPct / 100.0 && c >= bidW.price && c - l >= range * rejectPct / 100.0 { signal = 1 rested = ch.before traded = sold left = ch.after wallPrice = bidW.price + bidW.binSize / 2.0 wallEdge = bidW.price } else if showWalls && lost >= ch.before * gone { // Shrank: eaten by the sells there (filled) or pulled. let filled = sold + boughtAt(lo, hi) >= lost * 0.5 marks.get(str.format("b{0}", chart.index)).set(t, bidW.price, shape: filled ? shape.square : shape.cross, color: filled ? #ffd740 : #b0bec5, size: 6.0) } } } if signal == 0 && askW != null && h >= askW.price { let lo = askW.price - askW.binSize * 0.001 let hi = askW.top - askW.binSize * 0.001 let ch = book.change(askW.price + askW.binSize / 2.0) let bought = boughtAt(lo, hi) if ch != null && ch.before > 0.0 { let held = ch.after >= ch.before * holdPct / 100.0 let lost = ch.before - ch.after if allowShort && held && bought >= ch.before * hitPct / 100.0 && c <= askW.top && h - c >= range * rejectPct / 100.0 { signal = -1 rested = ch.before traded = bought left = ch.after wallPrice = askW.price + askW.binSize / 2.0 wallEdge = askW.top } else if showWalls && lost >= ch.before * gone { let filled = bought + tradedAt(lo, hi) >= lost * 0.5 marks.get(str.format("a{0}", chart.index)).set(t, askW.top, shape: filled ? shape.square : shape.cross, color: filled ? #ffd740 : #b0bec5, size: 6.0) } } } if !flat || signal == 0 { return } // The trade: the stop behind the wall, sized by the risk to it. let sign = signal > 0 ? 1.0 : -1.0 let stop = wallEdge - sign * stopBuffer * atr let risk = (c - stop) * sign if risk <= 0.0 { return } let rTarget = c + sign * targetR * risk let target = rTarget if targetMode == "Opposite wall" { let floorT = wallFloor(targetPct, 0) let other = signal > 0 ? book.wallAbove(targetPct, minSize: floorT, unit: bookunit.quote) : book.wallBelow(targetPct, minSize: floorT, unit: bookunit.quote) if other != null { // Just before the other wall, at least one R away. let before = signal > 0 ? other.price : other.top target = (before - c) * sign >= risk ? before : rTarget } } let qty = math.min(strategy.equity * riskPct / 100.0 / risk, strategy.equity * maxExposure / c) if qty <= 0.0 { return } let name = signal > 0 ? "Absorbed long" : "Absorbed short" strategy.entry(name, signal > 0 ? strategy.long : strategy.short, qty: qty, comment: str.format("wall {0:.2} held {1:.0}%", wallPrice, left / rested * 100.0)) strategy.exit(name + " exit", name, stop: stop, limit: target, comment: "SL / TP") nextId += 1 tradeDir = signal entryPrice = c stopPrice = stop targetPrice = target tradeFrom = t drawTrade(t) if showWalls { marks.get(str.format("s{0}", chart.index)).set(t, signal > 0 ? l - 0.2 * atr : h + 0.2 * atr, shape: signal > 0 ? shape.up : shape.down, color: signal > 0 ? longColor : shortColor, size: 8.0) let text = str.format("{0} · wall {1:.2}\nhit {2:.2} of {3:.2}\nleft {4:.0}%", signal > 0 ? "Long" : "Short", wallPrice, traded, rested, left / rested * 100.0) labels.get(nextId).set(t, signal > 0 ? l - 0.7 * atr : h + 0.7 * atr, text, color: signal > 0 ? longColor : shortColor) } let payload = Absorbed { side: signal > 0 ? "long" : "short", wall: wallPrice, rested: rested, traded: traded, left: left } if signal > 0 { longAlert.trigger(payload) } else { shortAlert.trigger(payload) } } on chart.update { let status = strategy.positionSize > 0.0 ? " · long" : strategy.positionSize < 0.0 ? " · short" : "" script.setTitle(str.format("Wall Absorption{0}", status)) } ``` Open a liquid market on 1m to 5m, with the Heatmap if you want to see the walls, and add the script. The Strategy Tester opens with the backtest. ## How it works ### The wall `book.wallBelow(searchPct, minSize: …, nearest: true, unit: bookunit.quote, barsAgo: 1)` finds the nearest heatmap cell below the mid, on the bar before, that holds at least `wallMult` times the average cell within the search range and at least `minWallUsd`. `wallFloor` computes the threshold from `book.liquidity` and `book.cellSize`. A cell is what the heatmap draws for that bar and price: the mean resting size at the bar's minute closes, summed in the heatmap's bin. ### Hit and held | Check | Read | Default | |---|---|---| | Price reached the wall | the bar's low against the cell's top | — | | Market orders hit it | sells traded at the cell's prices: `vol.summary(from:, to:).sell` | ≥ 20 % of the wall | | It held | `book.change(price).after` against `.before` | ≥ 60 % left | | Price failed to break | the close not below the wall, `rejectPct` of the range off the low | 30 % | Shorts mirror it at an ask wall with the buys traded there. ### Pulled or filled A wall that lost at least `goneShare` of its size while price was at it is marked: a cross when little traded at its prices (pulled, maybe spoofed), a square when the volume there accounts for at least half of what it lost (filled, a real breakout). ### Exits and size | Exit | Rule | |---|---| | Stop | Behind the wall's cell by `stopBuffer` ATR | | Target | `targetR` times the risk, or the largest wall on the other side within `targetPct` (at least 1 R) | | Time stop | `strategy.closeAll` after `maxBars` bars | The size risks `riskPct` of the equity, capped at `maxExposure` times the equity; commission 0.05 % a side, slippage 2 ticks, funding charged. ### What the minute book cannot see The book is recorded once a minute. A wall placed and pulled inside a minute never shows, and on a 5m bar a wall that stood two minutes counts two fifths of its size. Absorption shows best on 1m bars. ## What we measured On the BTCUSDT Binance perpetual with the data hub's history from Thursday 15 October 14:52 to Friday 16 October 19:15 UTC (about 28 hours), at the defaults: no trade on 1m or 5m. Qualifying walls (mostly $12M to $30M at round prices) were touched 23 times on 1m; the ones hit hard were filled, the others barely traded or were pulled. With looser thresholds (hit 10 %, held 40 %, rejection 20 %, search 1 %, size 3×) there were two trades on 1m (+$68.62 and −$63.72, net +$4.89 on $10,000) and two losers on 5m (−$110.11). A day of data cannot tell whether the setup has an edge: treat these as a check that the mechanics work, and test on a longer history. ## Adapting it - **Several books.** `Market: Binance + Bybit + OKX` sums the books and the footprints of the three venues; spot books (`Binance spot + Coinbase`) hold the unleveraged walls. - **Only refills.** Set `holdPct` above 100 to trade walls that grew while hit. - **Liquidation context.** Require a liquidation cluster beyond the wall with `data.liqmap`: a wall in front of a magnet holds less often. ## Related - [Order book and liquidation maps](/docs/scripting/guides/heatmap-data) - [Liquidation sweep reversal](/docs/scripting/examples/liquidation-sweep-reversal) - [Strategies & backtesting](/docs/scripting/strategies) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/wall-absorption --- # Seconds order flow > Normalize recorded volume and delta on a 10-second chart, with confirmed-bar updates. Choose **10 seconds** in the chart interval menu. The same script works at 1s, 5s, 15s, 30s and 45s. Seconds bars and footprint rows come from recorded trades; a minute candle cannot supply the trades inside its six 10-second bars. Missing intervals stay absent. Check the installation's historical coverage before comparing sessions or running a strategy. `ta.zscore(source, len)` returns the current sample's distance from the rolling population mean in standard deviations. It includes the current sample, waits for `len` finite samples, ignores missing samples and returns zero for a constant window. Like other stateful `ta.*` functions, forming evaluations do not alter confirmed state. Lengths from 1 to 10000 are supported. ```flowscope title="seconds-flow.fs" script "Seconds order flow" input length = input.int(60, title: "Window in bars", min: 2, max: 10000) data chart = subscribe(data.ohlcv) pane flow = pane(title: "Volume and delta deviations", height: 0.25) plot ( volume = plot.line(title: "Volume z-score", color: color.blue, on: flow) delta = plot.histogram(title: "Delta z-score", color: color.orange, on: flow) ) on chart.close { volume.plot(ta.zscore(chart.volume, length)) delta.plot(ta.zscore(chart.buyVolume - chart.sellVolume, length)) } ``` A 60-bar window at 10s represents ten minutes of bars when every interval traded. Missing bars make the elapsed time longer. The first 59 samples have no output. Large deviations describe unusual measured activity; they do not establish a trade direction by themselves. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/examples/seconds-flow --- # Performance > How chart scripts run, what the step budget measures, and the habits that keep heavy scripts responsive. Most scripts are fast without any tuning. Write the clearest version first, check that it is correct, and only then look for repeated work that is worth removing. This page explains how scripts are executed on a chart, what the step budget measures, and the handful of habits that keep heavy scripts responsive. ## How a chart runs a script The first run replays the chart's loaded history through your handlers, bar by bar from the first bar to the live edge, and then lays the outputs out on the chart. Flowscope starts a new run when: - the code or an input changes, - bars are added (the chart loads more history or a new bar opens), or - the forming bar changes and the script reads live values. Lightweight scripts refresh up to every 100 ms; expensive ones back off up to 750 ms based on measured computation time. Indicators with only close handlers reading local OHLCV, VD or CVD skip these intrabar replays. New bars and history changes still trigger a run. Runs happen on the engine's worker threads, not on the UI thread: the chart keeps showing the previous output until the new run is done, and a slow script cannot stall panning, the heatmap or the DOM. In the browser build, history replay runs in small cooperative steps on the page thread and yields between clock events, allowing market updates, input and painting between steps. A single handler is still atomic: an expensive loop in one handler can still stall a frame. The assistant's script results report computation time, excluding the waits between steps. A run keeps the point where its confirmed history ended. The next run continues from there: only the bars confirmed since run, then the forming bar. The outputs are exactly what a replay of everything would produce, because a run checks first that every bar it already ran is unchanged. A whole replay happens only when the code, an input or the history itself changes (older bars load, a corrected bar, another interval, a bar replay), and for strategies, whose broker starts over each run. So the first run costs the work per bar times the loaded bars (typically well under a microsecond per bar; 100 000 bars replay in a few tens of milliseconds), and the live refreshes after it cost only the newest bars. ## See what a script costs {#metrics} **Metrics…** in a script's legend menu opens its live report: - **Last run**: how long it took, how many bars it evaluated and whether it continued the previous run or replayed the whole history. - **Steps / eval** and **Output / eval**: histograms over every handler evaluation so far, on a logarithmic axis (a bar at 2–3 counts evaluations that took 2 or 3 steps). Below each, the evaluations and outputs per second of computation. - **Series**: every subscription the script reads: its market and interval, the history it asked for, the bars delivered, the live updates pushed, the rows held and the memory they take. - **Limits**: what the script asked for or used against what it was granted (history depth and span, the history step budget, the most steps one update took, the memory its values hold). A limit it reached is counted at the top. - **Subscriptions**: their keys and kinds. **Pause** freezes the report, **Copy** puts it on the clipboard as text, **Logs** shows what the script logged. ## Stay inside the step budget {#stay-inside-the-step-budget} Every handler run may spend **10 million steps**, and the replay of the loaded history **100 million** in all, across the runs that continue it (a quarter of that in the browser, where the replay shares the page's thread). A step is one unit of evaluation work: an operator, a function call, a field read, a loop iteration. Built-ins that walk a collection (`includes`, `sum`, `indexOf`…) also pay one step for every 16 elements they look at. Ordinary handlers use a few hundred steps; the budgets only matter for loops, and especially nested loops. If a handler exceeds a budget the script stops with a budget error in its status. Edit the script to reduce the work. The usual causes are: | Cause | Fix | |---|---| | A loop over a long window on every bar | Use a `ta.*` function, or a `rolling` with `sum()`, `max()`, `sumLast(n)` | | Rebuilding a collection in every handler call | Keep it in `state` and update it with `push`, `shift`, `set` | | Drawing current-only objects on every historical bar | Draw under `chart.isLast` in an update handler | | Nested loops over levels and bars | Keep per-level results in a `map` and update only what changed | ## Reuse work within a handler When several outputs need the same value, compute it once and share the local: ```flowscope title="shared-work.fs" script "Distance from VWAP" data chart = subscribe(data.ohlcv) pane dpane = pane(title: "Distance from VWAP, %", height: 0.2) plot ( vwapLine = plot.line(title: "VWAP", color: color.amber, width: 2) distance = plot.histogram(title: "Distance", on: dpane) ) on chart.update { let close = chart.close let vwap = chart.vwap() let pct: float? = close == null || vwap == null || vwap == 0.0 ? null : (close / vwap - 1.0) * 100.0 vwapLine.plot(vwap) distance.plot(pct, color: (pct ?? 0.0) >= 0.0 ? color.green : color.red) } ``` Calling `chart.vwap()` twice would not only do the work twice; each call site keeps its own history, so you would also maintain two identical accumulators. ## Prefer rolling functions `ta.*` functions keep exactly the history they need and update it incrementally. Rebuilding the same window by hand on every bar is both slower and longer. ```flowscope // slow: scans the whole window on every bar state highs = rolling(200) on chart.close { highs.push(chart.high) let top = highs.reduce(0.0, |acc, v| math.max(acc, v ?? 0.0)) } ``` ```flowscope // fast: incremental, and null-aware on chart.close { let top = ta.highest(chart.high, 200) } ``` When you do need your own window, `rolling` with its built-in aggregates (`sum`, `avg`, `min`, `max`, `stdev`, `sumLast`) is the next best choice. Reach for `map`, `filter` and `reduce` when the transformation is genuinely custom, and keep the collections short. ## Bound collections and drawing pools Every collection has a capacity fixed when it is made, at most 50 000 elements. Choose capacities that match what the script uses or shows: - A rolling window needs exactly its length: `rolling(len)`. - A map of price levels needs as many entries as levels you display, not every level ever seen. Remove stale keys with `delete`. - An array that is full stops the script on the next `push`; check `isFull` and `shift()` first. Drawing pools have the `max` you give them, and a full pool recycles its oldest drawing. Treat keys as identities: - Use a **stable key** for an object that represents the same thing over time, such as `"day-high"`. Each `get(key).set(...)` moves the existing drawing instead of creating a new one. - For a stream of historical marks, use a counter or the bar's time as the key and let the pool's `max` decide how many stay. - Call `pool.delete(key)` when a drawing no longer means anything. ## Subscribe only to what you use Each subscription adds work to every run: - **Other timeframes** are built from the chart's bars, so they are cheap; they must be whole multiples of the chart's interval. - **Trades** are copied from the chart's tape into each run and delivered print by print. Filter them at the source with `minSize:` when you can, so the handler only sees the prints you care about. - **Order books** are read from the chart's recording once per period the script asks about, and kept for the run. - **Volume profiles** come from footprints; while a script reads them, the chart loads footprints for the bars on screen, which costs memory and network. Do not add a subscription until the calculation reads it. ## Draw current-only objects at the live edge Objects that only describe "now", such as today's levels, a status card or a projection to the right of price, need not be drawn on every historical bar. Compute on every bar, draw only under `isLast`: ```flowscope title="live-edge-levels.fs" script "Live day levels" data ( chart = subscribe(data.ohlcv) daily = subscribe(data.ohlcv, timeframe: 1D) stats = subscribe(data.stat) ) state ( levels = entities.linePool(max: 2) card = entities.labelPool(max: 1, anchor: anchor.topRight) longs = 0.0 shorts = 0.0 previousTime: time? = null ) on stats.close { // Today's liquidations, restarting with each day. let t = stats.time ?? time.fromUnixSeconds(0) let newDay = time.isNewPeriod(t, previousTime, 1D) previousTime = t longs = (newDay ? 0.0 : longs) + (stats.sellLiq ?? 0.0) shorts = (newDay ? 0.0 : shorts) + (stats.buyLiq ?? 0.0) } on chart.update { let hi = daily.forming.high let lo = daily.forming.low let start = daily.forming.time let t = chart.time if chart.isLast && hi != null && lo != null && start != null && t != null { levels.get("day-high").set(start, hi, t, hi, color: color.green, style: linestyle.dashed, extend: extend.right) levels.get("day-low").set(start, lo, t, lo, color: color.red, style: linestyle.dashed, extend: extend.right) let text = str.format("Range {0:.1}\nLongs liquidated {1:,.0}\nShorts liquidated {2:,.0}", hi - lo, longs, shorts) card.get("today").set(12.0, 12.0, text) } } ``` `isLast` is false throughout the history, so none of the drawing calls run there. Keep historical drawings, such as past swing labels, outside the condition if they should stay visible. ## Put confirmed work in close handlers An update handler runs on every change of the forming bar. Work that only needs the final value of a period, such as pushing into windows, counting events or updating per-period statistics, belongs in `on x.close`, which runs once per period. See [repainting](/docs/scripting/guides/repainting). ## Keep handlers readable Performance problems are easiest to find in handlers that are easy to read: - Move configuration into `input` and derived constants into `setup`, where they are computed once. - Name intermediate values with `let` instead of nesting long expressions. - Extract repeated logic into a [function](/docs/scripting/guides/functions); remember that each call site of a function containing `ta.*` keeps its own history. ## Checklist 1. Is each expensive value computed once per handler run? 2. Are rolling questions answered with `ta.*` or `rolling` aggregates rather than loops? 3. Are collections in `state`, bounded, and updated in place? 4. Do live-only drawings sit under `isLast`, with stable keys? 5. Does every subscription earn its place? See the [limits table](/docs/scripting/reference#limits) in the language reference for every hard limit in one place. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/performance --- # Language reference > The precise rules of Flowscope Script, from lexical structure and declarations to types, operators, evaluation order, null semantics and limits. This page defines the language. It is written to be exact rather than gentle; for explanations and worked examples, start with the [guides](/docs/scripting/guides). Every built-in function, constant and record is in the [built-in catalog](/docs/scripting/reference/catalog), every compiler message in [diagnostics](/docs/scripting/reference/diagnostics). Syntax is shown in a compact notation: `name` stands for an identifier, `expr` for an expression, `T` for a type and `...` for repetition. ## Source files A script is a single UTF-8 text of at most 1 000 000 bytes. Its first line is the header: ```flowscope script "Funding watch" ``` The string is the display name; `script.setTitle(text)` replaces it at run time. Everything after the header is a sequence of **top-level declarations**. There are no statements at top level, no imports and no other entry point. ## Lexical structure ### Comments A comment starts with `//` and runs to the end of the line. There are no block comments. ### Lines and statements Statements and declarations end at the end of the line. There are no semicolons and exactly one statement per line. A long expression continues on the next line when the line ends with a binary operator, `?`, `:`, `,`, `(`, `[` or `{`. A line must not begin with an operator, `?` or `:`. ```flowscope let body = (chart.close ?? 0.0) - (chart.open ?? 0.0) let score = body / context.tickSize + (chart.volume ?? 0.0) / 1000.0 ``` Inside parentheses, brackets and grouped declaration blocks, newlines between elements are allowed. ### Identifiers An identifier starts with an ASCII letter or `_` and continues with letters, digits or `_`. Identifiers are case-sensitive. User type names start with an upper-case letter. Built-in namespace names (`ta`, `math`, `str`, `color`, `time`, `input`, `plot`, `fill`, `data`, `context`, `entities`, `log`, `strategy`, …) are predefined. ### Keywords ```text script data input state setup plot fill window pane alert type fn on let if else match for in return true false null ``` `self` names the receiver inside methods of a `type`. `strategy(...)` at top level is the [strategy declaration](/docs/scripting/strategies/declaration). ### Number literals | Form | Type | Examples | |---|---|---| | Digits without a decimal point | `int` (64-bit signed) | `0`, `14`, `1_000_000` | | Digits with a decimal point | `float` (64-bit IEEE 754) | `1.5`, `0.25`, `1_000.0` | `_` may separate digit groups. A float literal needs digits on both sides of the point. A leading `-` is the unary minus operator. ### String literals Strings are enclosed in double quotes and may not span lines. Escapes: `\n`, `\t`, `\"` and `\\`; any other is an error. There is no interpolation; use `str.format("{0} / {1:.2}", a, b)`. ### Boolean and null literals `true` and `false` have type `bool`. `null` is the absent value; its type comes from context (see [null semantics](#null-semantics)). ### Color literals `#RRGGBB` is an opaque colour, `#RRGGBBAA` a colour with alpha (`00` transparent to `FF` opaque). ### Timeframe and duration literals A positive integer followed by a unit is a `timeframe` where a timeframe is expected and a `duration` where a duration is expected: | Unit | Meaning | Examples | |---|---|---| | `s` | seconds | `30s` | | `m` | minutes | `1m`, `15m` | | `h` | hours | `1h`, `4h` | | `D` | days | `1D` | | `W` | calendar weeks | `1W` | | `M` | calendar months | `1M` | | `Y` | calendar years | `1Y` | Units are case-sensitive: `1m` is one minute, `1M` one month. `60m` normalises to `1h`. `tf("15m")` converts a string. Calendar units (`W`, `M`, `Y`) are valid for subscriptions but not as indicator lengths. `0s` is a valid duration: `history: 0s` means live data only. ### Sessions A `session` is a daily UTC window, built with `time.session("09:30", "16:00")` or `time.sessionMinutes(570, 960)`. An end before the start wraps past midnight. ## Declarations All declarations appear at top level. `input`, `data`, `state`, `setup`, `plot`, `fill`, `pane` and `alert` also have a **grouped form** with one declaration per line: ```flowscope input ( fastLength = input.int(12, title: "Fast", min: 1, max: 200) slowLength = input.int(26, title: "Slow", min: 2, max: 400) ) ``` ### script `script "Name"`, exactly once, first. ### input `input name = input.kind(default, title: ..., ...)` declares a setting, fixed for a run. Layout rows `input.tab(title:)`, `input.group(title:)` and `input.section(title:)` are written without a binding. Each effective key (`key:` or the binding name) must be unique. See [inputs and visuals](/docs/scripting/guides/inputs-and-visuals). ### data ```text data name = subscribe(data.source, exchange: ..., symbol: ..., timeframe: ..., history: ..., bucket: ..., minSize: ..., map: ..., minLeverage: ..., maxLeverage: ...) ``` Declares a subscription. Omitted `exchange`, `symbol` and `timeframe` follow the chart. Arguments must be setup values. `minSize:` applies to `data.trades`, `bucket:` to `data.vd` and `data.cvd`, `map:`, `minLeverage:` and `maxLeverage:` to `data.liqmap`. See [data and update events](/docs/scripting/guides/data-and-events) and [order book and liquidation maps](/docs/scripting/guides/heatmap-data). `bucket:` filters volume delta by trade size, by each trade's notional in USD: 1 every trade (the default), 2 $1–1K, 3 $1K–10K, 4 $10K–25K, 5 $25K–50K, 6 $50K–100K, 7 $100K–250K, 8 $250K–500K, 9 $500K–1M, 10 $1M–5M, 11 $5M and more. The subscription's `bucket`, `bucketLabel`, `bucketMinUsd` and `bucketMaxUsd` name the group. Bars before the session have their sizes from the recorded minutes, as far back as the book history; a bar whose trades are not all known reads `null`. ### setup ```text setup name = expr setup name = if cond expr setup name = for k in collection { expr } ``` Computes a value once at start. The `if` form has no `else` and yields `T?`. The `for` form builds a **family**: a read-only map from each `k` to the body's value, such as a set of plots. ```flowscope setup names = ["Fast", "Slow"] setup lines = for n in names { plot.line(title: n) } setup onBinance = if context.exchange == "binancef" 1.0 ``` ### state `state name = expr` or `state name: T = expr` declares a variable initialised once and kept across handler calls. The initialiser must be a setup expression. Handlers may assign only to state. ### plot `plot name = plot.kind(title: ..., color: ..., on: ..., ...)` declares a plot. Kinds: `line`, `candle`, `bar`, `histogram`, `point`, `marker`, `arrow`, `label`, `bg`. Feed it from handlers with `name.plot(value)` (`name.plot(o, h, l, c)` for candles and bars). Without `on:` a plot draws on the price pane. ### fill `fill name = fill.between(a, b, color: ...)` shades between two line plots. ### pane `pane name = pane(title: ..., height: ...)` declares a pane under the price chart sharing its time axis; `height` is its share of the chart (default `0.3`). ### window ```text window name = window(title: ...) { panelName: chart.panel(...), ... } ``` Declares a separate window with named panels: `chart.panel`, `table.new` or `heatmap.new`. Panels are addressed as `name.panelName`. See [custom axes and charts](/docs/scripting/guides/custom-axes-and-charts). ### alert `alert name = alert(title: ..., onClose: ..., payload: TypeName)` declares an alert; the binding name is its identity. Fire it from a handler with `name.trigger()` or `name.trigger(TypeName { ... })`. Alerts fire on live data only, at most once per bar. ### type ```text type Name { field: T ... fn method(params) -> T { ... } } ``` Declares a record type, optionally with methods. Values are created with a struct literal and copied on assignment. See [user-defined types](/docs/scripting/guides/user-defined-types). ### fn ```text fn name(param: T, param: T = literal) -> R { statements } fn name(param: T) -> (R1, R2) { statements } fn name(param: T) { statements } ``` Parameter types are required; defaults must be literals and follow required parameters. Each call site of a function that calls `ta.*` keeps its own indicator state. User functions cannot be called from setup expressions. See [functions](/docs/scripting/guides/functions). ### on ```text on sub.open { statements } on sub.update { statements } on sub.close { statements } on sub.trade(name) { statements } ``` `trade` is for `data.trades` subscriptions. `data.book`, `data.volume` and `data.liqmap` have no handlers. At most one handler per subscription and event. ## Types ### Primitive types | Type | Values | Notes | |---|---|---| | `int` | 64-bit signed integers | Counts, indices, keys | | `float` | 64-bit floating point | Prices, volumes, ratios | | `bool` | `true`, `false` | Conditions need a non-null `bool` | | `string` | UTF-8 text | Immutable | | `color` | RGBA colour | Literals, `color.*` | | `time` | A point in time, UTC | `chart.time` | | `duration` | A span of time | `2h`, `time.days(3)`; `time + duration` is a `time` | | `timeframe` | A bar period | `4h`, `tf("15m")`, `context.timeframe` | | `session` | A daily time window | `time.session("08:00", "16:00")` | Arithmetic operands must have the same numeric type. Convert explicitly with `float(x)`, `int(x)` (truncates; `null` for non-finite or out-of-range values) and `string(x)`. ### Nullable types `T?` is `T` or `null`. Market fields are nullable. A non-null value is accepted where `T?` is expected; the reverse needs narrowing or `??`. ### Collections | Type | Constructor | Behaviour | |---|---|---| | `T[]` | `float[](n)` | Array with capacity `n`, length 0. Adding beyond capacity stops the script. | | `rolling` | `rolling(n)` | Fixed window; pushing when full drops the oldest element. | | `map` | `map(n)` | Key-value store with capacity `n`. | - Capacities must be setup values and at most 50 000. - Setup literals `[1.0, 2.0]` and `{"a": 1}` create read-only collections. - `float[]` and `rolling` may hold `null`; other element types need non-null values. - Reads (`xs[i]`, `get`, `first`, `last`, `pop`, `shift`) return `T?`. `xs[i] = v` requires `i < xs.len`. - `map`, `filter` and `reduce` take lambdas; see the collections family in the [catalog](/docs/scripting/reference/catalog/collections). ### User types Field types may be any value type, including nullable and other user types, but not collections. Struct literals give every field. Records are values: assignment and argument passing copy them. ### Several return values A function may return a tuple `(T1, T2, ...)`: `return a, b` returns two values, and the caller destructures them at once with `let a, b = f()`: ```flowscope fn bands(mid: float, dev: float, mult: float) -> (float, float) { return mid + dev * mult, mid - dev * mult } ``` ### Subscription records {#subscription-records} | Source | Record | Fields | |---|---|---| | `data.ohlcv` | `OhlcvSub` | `time open high low close volume buyVolume sellVolume buyCount sellCount trades` | | `data.oi` | `OiSub` | `time open high low close` | | `data.vd` | `VdSub` | `time open high low close`, bucket fields | | `data.cvd` | `CvdSub` | `time open high low close`, bucket fields | | `data.stat` | `StatSub` | `time sellLiq buyLiq markPrice fundingRate` | | `data.book` | `BookSub` | Snapshot queries (`data.book.*` methods) | | `data.volume` | `VolumeSub` | Snapshot queries (`data.profile.*` methods) | | `data.liqmap` | `LiqmapSub` | Level-map queries (`data.liqmap.*` methods, a Flowscope extension) | | `data.trades` | `TradesSub` | Delivers `Trade { time, price, size, isBuy }` to `on t.trade(tr)` | Period records have `[n]` history on their value fields, a `forming` view of the developing period and status fields (`isNew`, `isRealtime`, `isHistory`, `isFirst`, `isLast`, `index`). Subscription methods include `atr`, `tr`, `vwap`, `vwma`, `mfi`, `accDist`, `change` and `delta`. Full lists are in the [data family](/docs/scripting/reference/catalog/data). ### Constant namespaces Enumerated arguments are namespaced constants: `linestyle.*`, `shape.*`, `anchor.*`, `extend.*`, `align.*`, `axis.*`, `font.*`, `easing.*`, `colorspace.*`, `volbasis.*`, `bookunit.*`, `liqmap.*`, and the strategy constants `strategy.long`, `strategy.short`, `strategy.fixed`, `strategy.cash`, `strategy.percentOfEquity`, `strategy.commission.*` and `strategy.oca.*`. ## Operators From lowest to highest precedence: | Level | Operators | Meaning | |---|---|---| | 1 | `? :` | Conditional (right-associative) | | 2 | `\|\|` | Logical or, short-circuit | | 3 | `&&` | Logical and, short-circuit | | 4 | `==` `!=` | Equality | | 5 | `<` `<=` `>` `>=` | Ordering | | 6 | `??` | Null coalescing (right-associative) | | 7 | `+` `-` | Add, subtract | | 8 | `*` `/` | Multiply, divide | | 9 | `!` `-` (unary) | Not, negate | | 10 | `.` `f( )` `[ ]` | Member access, call, index and history | Rules: - `??` binds tighter than comparisons: `x ?? 0.0 > 5.0` is `(x ?? 0.0) > 5.0`. - `??` mixed with `&&` or `||` needs parentheses: `(x ?? false) && y`. - There is no `%`, no bitwise operators, no `**` (use `math.pow`) and no `++`. - `&&`, `||` and `!` take non-null `bool` operands. - `..` appears only in `for` ranges. - Assignment is a statement, not an expression. ## Statements ### let ```text let name = expr let name: T = expr let a, b = f() ``` Binds an immutable local for the rest of the block. An annotation is needed when the initialiser does not fix the type, for example `let x: float? = null`. ### Assignment `target = expr`, `+=`, `-=`, `*=`, `/=`. The target must be a `state` binding, a field path rooted in one, an element of a state collection or, inside a method, `self` or one of its fields. ### if and else `if cond { ... } else if cond { ... } else { ... }`. `cond` must be a non-null `bool`; braces are required. Null checks narrow locals in the branch. ### for ```text for x in collection { ... } for i in a..b { ... } for k, v in map { ... } ``` The range form counts from `a` up to but not including `b`. There is no `while`, `break` or `continue`. Do not call `ta.*` inside a loop: each call site must run exactly once per bar. ### match ```text match subject { pattern => statement pattern => { statements } } ``` Arms are tried top to bottom; one arm per line, no commas. Patterns: literals and constants (`0`, `"bybitf"`, `bookunit.base`), comparisons (`> 0.0`, `<= -2.0`), `null`, alternatives joined with `|` (`0 | 1 | 2`), and `_` for anything. Patterns do not bind names or take guards. The statement form need not be exhaustive; the expression form must be. ### return `return`, `return expr` or `return a, b` leaves the function, method or handler. ### Expression statements A call whose result is unused may stand alone: `p.plot(v)`, `log.info("ready")`, `xs.push(v)`, `strategy.entry("L", strategy.long)`. ## Expressions ### Calls and named arguments Positional arguments first, then named arguments in any order: `ta.ema(chart.close, 20)`, `input.int(14, title: "Length", min: 1)`. ### Conditional and coalescing `cond ? a : b` evaluates only the chosen branch. `a ?? b` yields `a` unless it is null; `b` is evaluated only when needed. ### match expressions ```flowscope fn regime(rate: float?) -> string { return match rate { null => "unknown" > 0.0005 => "crowded long" < -0.0003 => "crowded short" _ => "neutral" } } ``` ### Struct literals `TypeName { field: expr, field: expr }`; every field must be given. ### Lambdas `|x| expr` and `|acc, x| expr`, as arguments to collection methods such as `map`, `filter` and `reduce`. The body is one expression and must be pure: no `ta.*`, no plotting or drawing, no state assignment. ### Indexing and history | Form | Meaning | |---|---| | `xs[i]` | Element `i` of a collection, `T?` | | `sub.field[n]` | A subscription field `n` periods back, `T?` | History applies to subscription value fields, not to locals or state; keep your own history in a `rolling`. ## Handlers and evaluation order {#handlers-and-evaluation-order} | Handler | Runs | Own fields read | |---|---|---| | `on x.open` | Once when a period of `x` starts | The forming period | | `on x.update` | When the forming period changes; once per period in history | The forming period | | `on x.close` | Once when a period of `x` is confirmed | The period just confirmed | | `on t.trade(tr)` | Once per trade, in time order | `tr` is the trade | - In `x`'s own `open` and `update` handlers, `x.field` reads the forming period; in its `close` handler, the period just confirmed. - In handlers of other subscriptions, `x.field` reads `x`'s last **confirmed** period and `x.forming.field` its developing one. ### Execution phases 1. **Setup.** Inputs, setup values, state initialisers and surfaces, once. 2. **History.** Loaded history is delivered bar by bar to the same handlers, in time order. 3. **New data.** On a chart, the script runs again over the history when new data arrives, at most every 0.75 s, so the newest bars reach the same handlers. ### Status fields | Field | True when | |---|---| | `x.isHistory` | During history replay | | `x.isRealtime` | While the forming period receives live updates | | `x.isFirst` | On the first loaded period | | `x.isNew` | In an update in which `x` confirmed a period | | `x.isLast` | In an update of the newest, live period; false in close handlers | | `x.index` | Period number from the first loaded period, from 0 | ## Setup and eval contexts | | Setup | Eval | |---|---|---| | Where | `input`, `data`, `setup`, `state` initialisers, surfaces, `alert`, `strategy`, parameter defaults | Handler, function and method bodies | | When | Once, at start | On every event | | Inputs, setup values, `context.*`, pure helpers | yes | yes | | Array and map literals | yes (read-only) | no; use constructors | | Surface and plot constructors | yes | no | | Subscription reads, `ta.*`, user `fn` calls | no | yes | | Plotting, drawing, alerts, orders, `log.*`, state assignment | no | yes | ## Null semantics {#null-semantics} 1. Every type `T` has a nullable form `T?`. Market fields and collection reads are nullable. 2. Arithmetic with a `null` operand yields `null`. 3. Ordering comparisons with a `null` operand yield `false`. 4. `a == b` with one side `null` is `false`; `a != b` is then `true`. 5. `a ?? b` replaces `null` with `b`. 6. Conditions need a non-null `bool`: test for null or use `?? false`. 7. After `if x != null`, the local `x` is narrowed to `T`. Narrowing applies to locals and parameters, not to subscription fields, state or record fields; copy those to a local first. 8. A bare `null` takes its type from context; `c ? null : null` is an error. 9. Plotting `null` leaves a gap. 10. There is no `na()`, `nz()`, `?.` or postfix `!`. ## Limits {#limits} | Limit | Value | |---|---| | Source size | 1 000 000 bytes | | Inputs | 100 | | Plots | 200 | | Fills | 50 | | Alerts | 50, with at most 16 payload fields | | Subscriptions | 30 | | User types | 128, at most 64 fields, nested at most 16 deep | | Collection capacity | 50 000 | | Map key length | 64 bytes | | Indicator length (`ta.*`) | 10 000 | | Nesting of expressions and blocks | 128 levels | | Steps per update | 10 000 000 | | Steps over the history replay | 100 000 000 (25 000 000 in the browser) | | Steps of the setup (all declarations) | 2 000 000 | | Function calls nested | 64 | | String length | 1 MB | | Memory held in values | 256 MB | | Entities per pool | 5 000 | | Table rows / columns | 5 000 / 64 | | Heatmap cells | 250 000 | | Log lines kept | the newest 1 000 | Exceeding a static limit is a compile error. Exceeding a capacity, a budget or a size at run time stops the script with an error that names it (`budget_exceeded`, `call_depth`, `string_too_long`, `memory_exceeded`). Scripts are shared between traders, so these limits keep any script, however written, from stalling or crashing the terminal. See [performance](/docs/scripting/performance). ## See also - [Built-in catalog](/docs/scripting/reference/catalog): every function, method, constant and record. - [Diagnostics](/docs/scripting/reference/diagnostics): every compiler message, with a failing and a fixed script. - [Guides](/docs/scripting/guides): explanations and complete examples. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference --- # Built-in catalog > Every built-in function, constant, data source and record of Flowscope Script, generated from the compiler's catalog. This catalog is generated from data, not written by hand: `catalog.json` lists every built-in with its signature, parameters and the contexts it may run in. Use Ctrl K to jump to any name, or open a family below. 588 entries in 19 families. | Family | Entries | Summary | |---|---|---| | [`script`](/docs/scripting/reference/catalog/script) | 1 | The script header and script-level helpers. | | [`input`](/docs/scripting/reference/catalog/input) | 11 | Parameters the trader edits in the script settings. | | [`data`](/docs/scripting/reference/catalog/data) | 94 | Subscriptions to market data and their snapshot queries. | | [`markets`](/docs/scripting/reference/catalog/markets) | 11 | Facts about exchanges and symbols. | | [`context`](/docs/scripting/reference/catalog/context) | 7 | The chart the script runs on. | | [`events`](/docs/scripting/reference/catalog/events) | 4 | Handler events that run script code when data arrives. | | [`records`](/docs/scripting/reference/catalog/records) | 33 | Records produced by subscriptions and snapshot queries. | | [`ta`](/docs/scripting/reference/catalog/ta) | 53 | Technical analysis: averages, oscillators, volatility, crosses and pivots. | | [`math`](/docs/scripting/reference/catalog/math) | 43 | Numeric helpers and constants. | | [`str`](/docs/scripting/reference/catalog/str) | 10 | String helpers. | | [`color`](/docs/scripting/reference/catalog/color) | 41 | Colours, gradients and colour scales. | | [`time`](/docs/scripting/reference/catalog/time) | 45 | Calendar, session, timeframe and duration helpers. | | [`collections`](/docs/scripting/reference/catalog/collections) | 37 | Arrays, rolling windows and maps with their methods. | | [`plot`](/docs/scripting/reference/catalog/plot) | 57 | Series plots, fills and their styles. | | [`surfaces`](/docs/scripting/reference/catalog/surfaces) | 19 | Panes, windows and their chart, table and heatmap panels. | | [`entities`](/docs/scripting/reference/catalog/entities) | 75 | Keyed drawings: lines, boxes, labels, arrows and more. | | [`alert`](/docs/scripting/reference/catalog/alert) | 2 | Declared alerts and their triggers. | | [`log`](/docs/scripting/reference/catalog/log) | 4 | Messages to the script log. | | [`strategy`](/docs/scripting/reference/catalog/strategy) | 41 | Strategy declaration, orders, position and results. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog --- # script reference > The script header and script-level helpers. Every script starts with a `script "Name"` header. Top-level declarations follow: `input`, `data`, `state`, `setup`, `plot`, `fill`, `pane`, `window`, `alert`, `strategy`, `type`, `fn` and `on` handlers. Statements run inside handlers and functions. ## Functions ### script.setTitle {#script-settitle} ```flowscope script.setTitle(text: string) -> void ``` Sets the title the chart shows for this script's indicator row. `text` sets the title shown for the indicator. You can call this on every update. The chart refreshes the title after the handler finishes when the text has changed. Pass an empty string to restore the name declared with `script "..."`. Titles are limited to 256 UTF-8 bytes; longer text is shortened. Changing the title only changes what is displayed. Saved settings, alerts, and the indicator list keep using the script name. To change the title of a pane you declared, call `paneName.setTitle(text)`. | Parameter | Type | |---|---| | `text` | `string` | **Available in** Handlers and functions called from handlers. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/script --- # input reference > Parameters the trader edits in the script settings. Declare inputs at top level with `input name = input.int(...)` or in a grouped `input ( ... )` block. Values are fixed for a run; changing one in the settings re-runs the script over the loaded history. `input.group`, `input.section` and `input.tab` organise long settings. ## Functions ### input.bool {#input-bool} ```flowscope input.bool(default: bool, title: string = "", key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, when: bool = true) -> bool ``` Adds a true-or-false input to the script settings. `default` sets the starting value. The result is never null. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `bool` | | `title` | `string` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `when` | `bool` | **Returns** `bool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.color {#input-color} ```flowscope input.color(default: color, title: string = "", key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, when: bool = true) -> color ``` Adds a color picker to the script settings. `default` sets the starting color. The result is never null. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `color` | | `title` | `string` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `when` | `bool` | **Returns** `color` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.exchange {#input-exchange} ```flowscope input.exchange(default: string, title: string = "", key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, filter: string[]? = null, when: bool = true) -> string ``` Adds a single-exchange picker to the script settings. `default` sets the starting exchange ID. The result is never null. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. - `filter` limits the available exchanges. Null allows all exchanges. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `string` | | `title` | `string` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `filter` | `string[]?` | | `when` | `bool` | **Returns** `string` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.exchanges {#input-exchanges} ```flowscope input.exchanges(default: string[], title: string = "", key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, minSelected: int? = null, maxSelected: int? = null, filter: string[]? = null, when: bool = true) -> string[] ``` Adds a multiple-exchange picker to the script settings. `default` sets the starting list of exchange IDs, in order. The result array is never null. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. - `filter` limits the available exchanges. Null allows all exchanges. - `minSelected` and `maxSelected` limit the number of selections. Values outside their allowed bounds are rejected before the script runs. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `string[]` | | `title` | `string` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `minSelected` | `int?` | | `maxSelected` | `int?` | | `filter` | `string[]?` | | `when` | `bool` | **Returns** `string[]` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.float {#input-float} ```flowscope input.float(default: float, title: string = "", min: float? = null, max: float? = null, key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, when: bool = true) -> float ``` Adds a decimal-number input to the script settings. `default` sets the starting value. The result is never null. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. - `min` and `max` limit the allowed values. Null leaves that end unrestricted. An out-of-range or non-finite `default` is rejected before the script runs. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `float` | | `title` | `string` | | `min` | `float?` | | `max` | `float?` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `when` | `bool` | **Returns** `float` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.group {#input-group} ```flowscope input.group(title: string = "", key: string? = null, collapsible: bool = false, collapsed: bool = false, when: bool = true) -> void ``` Groups controls in the script settings. `title` labels the group and `key` identifies it. - `collapsible` lets the user fold the group. - `collapsed` starts the group folded when true. - `when` hides the group when false. This adds a group and returns no value. | Parameter | Type | |---|---| | `title` | `string` | | `key` | `string?` | | `collapsible` | `bool` | | `collapsed` | `bool` | | `when` | `bool` | **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.int {#input-int} ```flowscope input.int(default: int, title: string = "", min: int? = null, max: int? = null, key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, when: bool = true) -> int ``` Adds a whole-number input to the script settings. `default` sets the starting value. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. - `min` and `max` limit the allowed values and help determine how much history calculations need. An out-of-range `default` is rejected before the script runs. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `int` | | `title` | `string` | | `min` | `int?` | | `max` | `int?` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `when` | `bool` | **Returns** `int` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.section {#input-section} ```flowscope input.section(title: string = "", key: string? = null, description: string? = null, sameLine: bool = false, when: bool = true) -> void ``` Adds a section heading to the script settings. `title` labels the section, `key` identifies it, and `description` adds help text. Optional settings can be left out. `sameLine` keeps the section on the same line; `when` hides it when false. This adds a heading and returns no value. The description is shown as text under the heading, as on an About tab. | Parameter | Type | |---|---| | `title` | `string` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `when` | `bool` | **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.select {#input-select} ```flowscope input.select(default: string, title: string = "", options: string[], key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, when: bool = true) -> string ``` Adds a dropdown of text options to the script settings. `default` sets the starting selection and should be one of the strings in `options`. The result is never null. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `string` | | `title` | `string` | | `options` | `string[]` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `when` | `bool` | **Returns** `string` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.tab {#input-tab} ```flowscope input.tab(title: string = "", key: string? = null) -> void ``` Adds a tab to the script settings. `title` labels the tab and `key` identifies it. The rows after it, up to the next tab, go on this tab of the settings; rows before the first tab go on the first. This adds a tab and returns no value. | Parameter | Type | |---|---| | `title` | `string` | | `key` | `string?` | **Available in** Setup only: declarations, `setup` and `state` initialisers. ### input.timeframe {#input-timeframe} ```flowscope input.timeframe(default: timeframe = 1h, title: string = "", key: string? = null, description: string? = null, sameLine: bool = false, required: bool = false, when: bool = true) -> timeframe ``` Adds a timeframe picker to the script settings. `default` sets the starting timeframe. The result is never null. Invalid selections are rejected before any handler runs. - `title` labels the control; `key` identifies it in saved settings. - `description` adds help text. - `sameLine` places the control on the same line as the previous control. - `required` marks the control as required. - `when` hides the control when false. Optional settings can be left out. The value stays fixed during each run. Changing it applies the new settings and may recalculate history. | Parameter | Type | |---|---| | `default` | `timeframe` | | `title` | `string` | | `key` | `string?` | | `description` | `string?` | | `sameLine` | `bool` | | `required` | `bool` | | `when` | `bool` | **Returns** `timeframe` **Available in** Setup only: declarations, `setup` and `state` initialisers. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/input --- # data reference > Subscriptions to market data and their snapshot queries. `data name = subscribe(data.ohlcv)` subscribes to the chart's own bars; `exchange`, `symbol` and `timeframe` arguments pick another market or period. Period sources (`ohlcv`, `oi`, `cvd`, `vd`, statistics) drive `on` handlers; `data.book`, `data.volume` and `data.liqmap` (liquidation and position level maps, a Flowscope extension) are snapshots read from another subscription's handler. Every source can read other markets: `exchange:` takes one exchange id, a `markets.agg(...)` selection or a list (an `input.exchanges` value), and several exchanges are combined into one market (volumes, open interest, liquidations and book levels added up, trades merged). On a chart, markets the data hub does not capture read null. ## Sources ### data.book {#data-book} ```flowscope source data.book -> BookSub ``` Provides order-book snapshots to read from another subscription's handler. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0` or `0s`) asks for live data only: no earlier snapshots are loaded, and reads return snapshots from the period the script started in onward. A negative constant is an error. Read snapshots from a handler on another subscription. An explicit timeframe sets the snapshot periods independently of the chart. `barsAgo` counts these source periods. Reads use the latest source boundary at or before the handler's time. Missing periods return null. Higher-timeframe reads use completed periods; they do not reveal the final contents of a period still in progress. Each book period holds what the Heatmap's cell of that bar holds: the mean resting size at the bar's sampled instants (every whole minute inside it and its close; the book is recorded once a minute). On one-minute bars that is the book at the minute's close; an order resting two minutes of five counts two fifths of its size. (MMT takes the period's latest snapshot instead; on one-minute bars both are the same.) Levels keep the recorded price buckets (`binSize()`); `cellSize()` is the Heatmap's bin, which `sizeAt`, `wallAbove`, `wallBelow` and `change` sum the levels in (Flowscope extensions). A positive `history` can raise the default history request but cannot reduce it. Initial snapshot history loads up to 250 source periods, separately from the chart's replay range, and is also limited by the source's available history. Several exchanges' books are summed: their levels are added up in price buckets of the widest exchange's bin. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | ### data.liqmap {#data-liqmap} ```flowscope source data.liqmap -> LiqmapSub ``` Provides liquidation and position level maps to read from another subscription's handler. - `map` picks the map: `liqmap.model` (default), the liquidation model of the Liquidation Heatmap: from open interest and taker flow it estimates where positions were opened and at which leverage (5x, 10x, 25x, 50x, 75x, 100x), and where they would be liquidated: an estimate, not real orders. Binance, Bybit and OKX futures and their aggregates have it. `liqmap.hlLiquidations`, `liqmap.hlStops` and `liqmap.hlTakeProfits`: Hyperliquid's maps of the chart's coin, built from the public positions and trigger orders of the accounts Flowscope tracks. - `minLeverage` and `maxLeverage` keep the model's levels of those tiers only (both included; null: no bound). The recorded levels carry no split by leverage, so the split of each level comes from the same model run again over the subscription's bars; a level that run does not hold counts by the tiers' weights. Not for Hyperliquid's maps. - `exchange`, `symbol`, `timeframe` and `history` as for `data.book`. Several exchanges' maps are added up. Each period holds the map's mean over the period, each level weighted by the time it stood, as the map's heatmap cells. Levels below the price of the period (long liquidations and stops, short take-profits) are `below()`, levels above `above()`, each nearest the price first, as coins at the level's price: a level's `value` is its USD notional. Every query reports notional. `price()` is the market's close at the end of the period, the default reference of the queries. Reads use completed periods, as the book's. Spot markets and venues without the model read null. The model's own history reaches as far as the hub recorded it. A Flowscope extension; MMT has no such source. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | | `map` | `LiqMap` | | `minLeverage` | `float?` | | `maxLeverage` | `float?` | ### data.cvd {#data-cvd} ```flowscope source data.cvd -> CvdSub ``` Subscribes to cumulative volume delta bars. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0`, `0s`, or `time.seconds(0)`) asks for live data only: nothing is loaded before the script goes live, handlers on this subscription run only on live updates, and reads return null until then. A live-only subscription does not make the chart's history finer. A negative constant is an error; a negative input value also means live data only. During the history replay its `isHistory` reads true. Once live, a live-only subscription finer than the chart adds its updates to the chart's live history, so the chart keeps fewer of its own bars after running for a while. - `bucket` selects a size group from 1 to 11, defaulting to 1. Other values are rejected before the script runs. Completed bars and the current forming bar are available. `history` can raise the amount of data loaded before the visible range so indicators have enough samples at the left edge. A positive value cannot reduce the default of 750 intraday bars. A longer history can help an EMA settle. Calculations still return null until they have enough samples. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | | `bucket` | `int` | ### data.ohlcv {#data-ohlcv} ```flowscope source data.ohlcv -> OhlcvSub ``` Subscribes to price and volume bars. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0`, `0s`, or `time.seconds(0)`) asks for live data only: nothing is loaded before the script goes live, handlers on this subscription run only on live updates, and reads return null until then. A live-only subscription does not make the chart's history finer. A negative constant is an error; a negative input value also means live data only. During the history replay its `isHistory` reads true. Once live, a live-only subscription finer than the chart adds its updates to the chart's live history, so the chart keeps fewer of its own bars after running for a while. The chart subscription (no exchange, symbol, or timeframe) cannot be live only. A live-only subscription with the same market and timeframe as one the script needs for a `.forming` read of a longer timeframe loads history like a normal subscription. Completed bars and the current forming bar are available. `history` can raise the amount of data loaded before the visible range so indicators have enough samples at the left edge. A positive value cannot reduce the default of 750 intraday bars. A longer history can help an EMA settle. Calculations still return null until they have enough samples. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | ### data.oi {#data-oi} ```flowscope source data.oi -> OiSub ``` Subscribes to open interest bars. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0`, `0s`, or `time.seconds(0)`) asks for live data only: nothing is loaded before the script goes live, handlers on this subscription run only on live updates, and reads return null until then. A live-only subscription does not make the chart's history finer. A negative constant is an error; a negative input value also means live data only. During the history replay its `isHistory` reads true. Once live, a live-only subscription finer than the chart adds its updates to the chart's live history, so the chart keeps fewer of its own bars after running for a while. Completed bars and the current forming bar are available. `history` can raise the amount of data loaded before the visible range so indicators have enough samples at the left edge. A positive value cannot reduce the default of 750 intraday bars. A longer history can help an EMA settle. Calculations still return null until they have enough samples. Several exchanges' open interest is added up. On a chart, the bars before the session read the open interest the market recorded (its open and close; high and low the larger and smaller of them). | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | ### data.stat {#data-stat} ```flowscope source data.stat -> StatSub ``` Subscribes to market statistics bars. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0`, `0s`, or `time.seconds(0)`) asks for live data only: nothing is loaded before the script goes live, handlers on this subscription run only on live updates, and reads return null until then. A live-only subscription does not make the chart's history finer. A negative constant is an error; a negative input value also means live data only. During the history replay its `isHistory` reads true. Once live, a live-only subscription finer than the chart adds its updates to the chart's live history, so the chart keeps fewer of its own bars after running for a while. Completed bars and the current forming bar are available. `history` can raise the amount of data loaded before the visible range so indicators have enough samples at the left edge. A positive value cannot reduce the default of 750 intraday bars. A longer history can help an EMA settle. Calculations still return null until they have enough samples. Several exchanges' liquidations are added up; `fundingRate` is the mean of the perpetuals' rates. On a chart, the bars before the session read the liquidations the market recorded (the hub's history), as the Liquidations study shows them; before the market's context was recorded they read null, not 0. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | ### data.trades {#data-trades} ```flowscope source data.trades -> TradesSub ``` Delivers individual trades to a trade handler. - `exchange` and `symbol` choose the market. Null inherits the chart's market, the window's market, or the run settings when neither is present. - `minSize` keeps only trades with `size >= minSize`. Null applies no size filter. A non-positive or non-finite value is rejected before the subscription starts. - `history` requests earlier trades as a duration. Null uses the default; your data plan may cap the requested history. When every trades subscription on the market sets a positive `history`, only that much is loaded, however long the chart's range is. Zero (`0s` or `time.seconds(0)`) asks for live trades only: no earlier trades are loaded and the trade handler runs only for trades that arrive after the script goes live, not for earlier trades of the current bar. A negative constant is an error. Several exchanges' prints are merged in time order. | Parameter | Type | |---|---| | `exchange` | `string?` | | `symbol` | `string?` | | `minSize` | `float?` | | `history` | `duration?` | ### data.vd {#data-vd} ```flowscope source data.vd -> VdSub ``` Subscribes to volume delta bars. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0`, `0s`, or `time.seconds(0)`) asks for live data only: nothing is loaded before the script goes live, handlers on this subscription run only on live updates, and reads return null until then. A live-only subscription does not make the chart's history finer. A negative constant is an error; a negative input value also means live data only. During the history replay its `isHistory` reads true. Once live, a live-only subscription finer than the chart adds its updates to the chart's live history, so the chart keeps fewer of its own bars after running for a while. - `bucket` selects a size group from 1 to 11, defaulting to 1. Other values are rejected before the script runs. Completed bars and the current forming bar are available. `history` can raise the amount of data loaded before the visible range so indicators have enough samples at the left edge. A positive value cannot reduce the default of 750 intraday bars. A longer history can help an EMA settle. Calculations still return null until they have enough samples. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | | `bucket` | `int` | ### data.volume {#data-volume} ```flowscope source data.volume -> VolumeSub ``` Provides volume-profile snapshots to read from another subscription's handler. - `exchange`, `symbol`, and `timeframe` choose the market and period. Omit them or use null to inherit the chart's settings, the window's settings, or the run settings when there is no chart or window. - `history` requests earlier data as a number of source periods or a duration. Null uses the default. Zero (`0` or `0s`) asks for live data only: no earlier snapshots are loaded, and reads return snapshots from the period the script started in onward. A negative constant is an error. Read snapshots from a handler on another subscription. An explicit timeframe sets the snapshot periods independently of the chart. `barsAgo` counts these source periods. Reads use the latest source boundary at or before the handler's time. Missing periods return null. Higher-timeframe reads use completed periods; they do not reveal the final contents of a period still in progress. Higher timeframes may use wider price bins. Read their width with `binSize()`. There is no forming volume-profile view. A positive `history` can raise the default history request but cannot reduce it. Initial snapshot history loads up to 250 source periods, separately from the chart's replay range, and is also limited by the source's available history. Several exchanges' profiles are summed by price row. | Parameter | Type | |---|---| | `exchange` | `string\|string[]?` | | `symbol` | `string?` | | `timeframe` | `timeframe?` | | `history` | `int\|duration?` | ## Methods ### data.book.askRange {#data-book-askrange} ```flowscope data.book.askRange(from: float, to: float, barsAgo: int = 0) -> BookSideSummary? ``` Summarizes ask levels within a price range. `from` and `to` include both ends of the range. Reversed bounds or a range with no levels returns null. Unavailable book data also returns null. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. `avgPrice` is the quantity-weighted average price of the selected levels. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `from` | `float` | | `to` | `float` | | `barsAgo` | `int` | **Returns** `BookSideSummary?` **Available in** Handlers and functions called from handlers. ### data.book.asks {#data-book-asks} ```flowscope data.book.asks(barsAgo: int = 0) -> BookLevels? ``` Returns ask levels, starting with the best price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Levels are sorted lowest to highest price. A snapshot with no asks returns an empty view with `len == 0`. Read the results in a `for` loop or a local `let` variable within the handler. They cannot be saved in `state`, record fields, or collections, passed to another function, or returned from one. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `BookLevels?` **Available in** Handlers and functions called from handlers. ### data.book.asksTo {#data-book-asksto} ```flowscope data.book.asksTo(price: float, barsAgo: int = 0) -> BookSideSummary? ``` Summarizes ask depth from the best price to a chosen price. Includes ask levels at or below `price`. A price beyond the far end of the book includes the whole side. An empty selection or unavailable book data returns null. `avgPrice` is the quantity-weighted average of the selected prices. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `price` | `float` | | `barsAgo` | `int` | **Returns** `BookSideSummary?` **Available in** Handlers and functions called from handlers. ### data.book.bestAsk {#data-book-bestask} ```flowscope data.book.bestAsk(barsAgo: int = 0) -> float? ``` Returns the lowest ask price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A snapshot with no asks returns null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.bestBid {#data-book-bestbid} ```flowscope data.book.bestBid(barsAgo: int = 0) -> float? ``` Returns the highest bid price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A snapshot with no bids returns null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.bidRange {#data-book-bidrange} ```flowscope data.book.bidRange(from: float, to: float, barsAgo: int = 0) -> BookSideSummary? ``` Summarizes bid levels within a price range. `from` and `to` include both ends of the range. Reversed bounds or a range with no levels returns null. Unavailable book data also returns null. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. `avgPrice` is the quantity-weighted average price of the selected levels. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `from` | `float` | | `to` | `float` | | `barsAgo` | `int` | **Returns** `BookSideSummary?` **Available in** Handlers and functions called from handlers. ### data.book.bids {#data-book-bids} ```flowscope data.book.bids(barsAgo: int = 0) -> BookLevels? ``` Returns bid levels, starting with the best price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Levels are sorted highest to lowest price. A snapshot with no bids returns an empty view with `len == 0`. Read the results in a `for` loop or a local `let` variable within the handler. They cannot be saved in `state`, record fields, or collections, passed to another function, or returned from one. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `BookLevels?` **Available in** Handlers and functions called from handlers. ### data.book.bidsTo {#data-book-bidsto} ```flowscope data.book.bidsTo(price: float, barsAgo: int = 0) -> BookSideSummary? ``` Summarizes bid depth from the best price to a chosen price. Includes bid levels at or above `price`. A price beyond the far end of the book includes the whole side. An empty selection or unavailable book data returns null. `avgPrice` is the quantity-weighted average of the selected prices. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `price` | `float` | | `barsAgo` | `int` | **Returns** `BookSideSummary?` **Available in** Handlers and functions called from handlers. ### data.book.binSize {#data-book-binsize} ```flowscope data.book.binSize(barsAgo: int = 0) -> float? ``` Returns the width of each book price bin. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.depth {#data-book-depth} ```flowscope data.book.depth(outerPct: float, ref: number? = null, innerPct: float? = null, barsAgo: int = 0, levels: int = 0) -> BookDepth? ``` Summarizes order-book depth around a price. - `outerPct` sets the distance on either side of `ref`, as a percentage: from `ref * (1 - outerPct/100)` to `ref * (1 + outerPct/100)`. Values outside its allowed range stop the script with an error. - `ref` defaults to the mid price. Without both a bid and an ask, the default reference is unavailable and the result is null. - `innerPct` optionally excludes prices strictly inside a smaller band around the reference. Null or zero excludes nothing. - `levels` (default 0) counts only that many price levels on each side of the book, nearest the best bid and best ask first, before the price band applies; 0 counts every level. A level is one bucket of `binSize()` (for a book summed from several exchanges, one bucket of the summed book). It reproduces a feed limited to N levels per side; negative values count as 0. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. An empty price band or unavailable book data also returns null. Read this from another subscription's handler; book subscriptions have no handlers. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `outerPct` | `float` | | `ref` | `number?` | | `innerPct` | `float?` | | `barsAgo` | `int` | | `levels` | `int` | **Returns** `BookDepth?` **Available in** Handlers and functions called from handlers. ### data.book.imbalance {#data-book-imbalance} ```flowscope data.book.imbalance(outerPct: float, unit: BookUnit = 2, ref: number? = null, innerPct: float? = null, barsAgo: int = 0, levels: int = 0) -> float? ``` Returns the balance between bids and asks around a price. - `outerPct` sets the price band, as for `depth`. Values outside its allowed range stop the script with an error. - `unit` chooses quote value (`bookunit.quote`, the default) or base quantity (`bookunit.base`). - `ref` defaults to the mid price. `innerPct` defaults to null. - `levels` limits the count to the nearest levels of each side, as for `depth`; 0 (the default) counts every level. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Returns `(bid - ask) / (bid + ask)`. Quote units match `depth(...).ratioValue`; base units match `depth(...).ratioQty`. A zero total gives NaN. The result is null whenever the corresponding `depth` call is null. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `outerPct` | `float` | | `unit` | `BookUnit` | | `ref` | `number?` | | `innerPct` | `float?` | | `barsAgo` | `int` | | `levels` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.mid {#data-book-mid} ```flowscope data.book.mid(barsAgo: int = 0) -> float? ``` Returns the midpoint between the best bid and ask. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Calculates `(bestBid + bestAsk) / 2`. Returns null if either side is empty. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.numAsks {#data-book-numasks} ```flowscope data.book.numAsks(barsAgo: int = 0) -> int? ``` Counts the ask levels in a book snapshot. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A snapshot with no asks returns 0. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `int?` **Available in** Handlers and functions called from handlers. ### data.book.numBids {#data-book-numbids} ```flowscope data.book.numBids(barsAgo: int = 0) -> int? ``` Counts the bid levels in a book snapshot. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A snapshot with no bids returns 0. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `int?` **Available in** Handlers and functions called from handlers. ### data.book.range {#data-book-range} ```flowscope data.book.range(from: float, to: float, barsAgo: int = 0) -> BookDepth? ``` Summarizes both bid and ask levels within a price range. `from` and `to` include both ends of the range. Reversed bounds or a range with no levels returns null. Unavailable book data also returns null. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Your data plan may limit the levels available to read individually. Built-in summary methods use the full book, so their totals may differ from a loop over the levels available on your plan. | Parameter | Type | |---|---| | `from` | `float` | | `to` | `float` | | `barsAgo` | `int` | **Returns** `BookDepth?` **Available in** Handlers and functions called from handlers. ### data.book.spread {#data-book-spread} ```flowscope data.book.spread(barsAgo: int = 0) -> float? ``` Returns the gap between the best ask and bid. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Calculates `bestAsk - bestBid`. Returns null if either side is empty. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.time {#data-book-time} ```flowscope data.book.time(barsAgo: int = 0) -> time? ``` Returns the start time of the book snapshot period. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `time?` **Available in** Handlers and functions called from handlers. ### data.book.sizeAt {#data-book-sizeat} ```flowscope data.book.sizeAt(price: float, unit: BookUnit = 1, barsAgo: int = 0) -> float? ``` Returns what rests in the heatmap cell of a price. The bids and asks resting in the cell that holds `price`, in base units (`bookunit.base`, default) or quote value (`bookunit.quote`): the size of the wall at that price as the Heatmap shows it. 0 where nothing rests. Cells are the heatmap's: the levels summed in price bins of `cellSize()`, starting at whole multiples of it, as the Heatmap draws one cell per bar and bin. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension; MMT's catalog has no such method. | Parameter | Type | |---|---| | `price` | `float` | | `unit` | `BookUnit` | | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.liquidity {#data-book-liquidity} ```flowscope data.book.liquidity(from: float, to: float, unit: BookUnit = 1, barsAgo: int = 0) -> float? ``` Returns the liquidity resting in a price range. Bids and asks resting at prices from `from` to `to` (both included, either order), in base units or quote value: `range(from, to).totalQty` as one number. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension; MMT's catalog has no such method. | Parameter | Type | |---|---| | `from` | `float` | | `to` | `float` | | `unit` | `BookUnit` | | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.book.wallAbove {#data-book-wallabove} ```flowscope data.book.wallAbove(pct: float, minSize: float = 0.0, nearest: bool = false, ref: number? = null, unit: BookUnit = 1, barsAgo: int = 0) -> PriceBin? ``` Finds the largest wall above a price. The heatmap cell with the most resting size whose centre lies above `ref` (the mid by default) and at most `pct` percent away. With `nearest: true`, the nearest cell of at least `minSize` instead. Sizes and `minSize` are in `unit` (base by default; `bookunit.quote` for notional). Null when no cell qualifies. Above the mid these are asks: the resistance a buyer meets. A wall is real resting liquidity, but it can be pulled before price arrives; compare it with the next periods (`change`) and with the volume traded at its prices. Cells are the heatmap's: the levels summed in price bins of `cellSize()`, starting at whole multiples of it, as the Heatmap draws one cell per bar and bin. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension; MMT's catalog has no such method. | Parameter | Type | |---|---| | `pct` | `float` | | `minSize` | `float` | | `nearest` | `bool` | | `ref` | `number?` | | `unit` | `BookUnit` | | `barsAgo` | `int` | **Returns** `PriceBin?` **Available in** Handlers and functions called from handlers. ### data.book.wallBelow {#data-book-wallbelow} ```flowscope data.book.wallBelow(pct: float, minSize: float = 0.0, nearest: bool = false, ref: number? = null, unit: BookUnit = 1, barsAgo: int = 0) -> PriceBin? ``` Finds the largest wall below a price. As `wallAbove`, for cells whose centre lies below `ref`: the bids that support the price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension; MMT's catalog has no such method. | Parameter | Type | |---|---| | `pct` | `float` | | `minSize` | `float` | | `nearest` | `bool` | | `ref` | `number?` | | `unit` | `BookUnit` | | `barsAgo` | `int` | **Returns** `PriceBin?` **Available in** Handlers and functions called from handlers. ### data.book.change {#data-book-change} ```flowscope data.book.change(price: float, bars: int = 1, unit: BookUnit = 1, barsAgo: int = 0) -> BookChange? ``` Compares a heatmap cell's size between two periods. The cell holding `price` in the period `barsAgo` (`after`) against the period `bars` before it (`before`), in `unit`. A wall that shrank with little traded at its prices was pulled; one that shrank by about what traded there was filled. Read the volume traded at the cell from a `data.volume` subscription with `vol.summary(from:, to:)` over the cell's prices, just inside its edges (see the guide *Order book and liquidation maps*, Pulled or filled). Null when either period has no book. A Flowscope extension; MMT's catalog has no such method. | Parameter | Type | |---|---| | `price` | `float` | | `bars` | `int` | | `unit` | `BookUnit` | | `barsAgo` | `int` | **Returns** `BookChange?` **Available in** Handlers and functions called from handlers. ### data.book.cellSize {#data-book-cellsize} ```flowscope data.book.cellSize(barsAgo: int = 0) -> float? ``` Returns the heatmap's price bin of a period. The cell height the cell queries use: the Heatmap's automatic bin at the period's price (about 0.012 % of it on a round step, $10 on BTC near 80 000), whole buckets of the recorded book. `binSize()` is the recorded bucket. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension; MMT's catalog has no such method. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.bookLevels.get {#data-booklevels-get} ```flowscope data.bookLevels.get(index: int) -> BookLevel? ``` Reads an order-book level by index. `index` counts from 0, starting with the best price. A negative index or one at or beyond `len` returns null. `view[i]` is equivalent to `view.get(i)`. The level's `value` equals `price * qty`. | Parameter | Type | |---|---| | `index` | `int` | **Returns** `BookLevel?` **Available in** Handlers and functions called from handlers. ### data.liqmap.below {#data-liqmap-below} ```flowscope data.liqmap.below(barsAgo: int = 0) -> BookLevels? ``` Returns the levels below the price, nearest first. Long liquidations (and long stops, short take-profits on Hyperliquid's maps) below the period's price, highest price first. Each level's `qty` is in coins at its price, its `value` the notional. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `BookLevels?` **Available in** Handlers and functions called from handlers. ### data.liqmap.above {#data-liqmap-above} ```flowscope data.liqmap.above(barsAgo: int = 0) -> BookLevels? ``` Returns the levels above the price, nearest first. Short liquidations (and short stops, long take-profits) above the period's price, lowest price first. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `BookLevels?` **Available in** Handlers and functions called from handlers. ### data.liqmap.sizeAt {#data-liqmap-sizeat} ```flowscope data.liqmap.sizeAt(price: float, barsAgo: int = 0) -> float? ``` Returns the notional in the heatmap cell of a price. The USD notional of the levels in the cell holding `price`; 0 where there are none. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `price` | `float` | | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.liqmap.density {#data-liqmap-density} ```flowscope data.liqmap.density(outerPct: float, ref: number? = null, innerPct: float? = null, barsAgo: int = 0) -> LiqDensity? ``` Sums the levels below and above a price within a band. The notional of the levels from `ref * (1 - outerPct/100)` to `ref`, and from `ref` to `ref * (1 + outerPct/100)`, leaving out those strictly inside `innerPct` when given. `ref` defaults to the period's price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `outerPct` | `float` | | `ref` | `number?` | | `innerPct` | `float?` | | `barsAgo` | `int` | **Returns** `LiqDensity?` **Available in** Handlers and functions called from handlers. ### data.liqmap.densityAbove {#data-liqmap-densityabove} ```flowscope data.liqmap.densityAbove(pct: float, ref: number? = null, barsAgo: int = 0) -> float? ``` Returns the notional above a price within a distance. The notional of the levels from `ref` up to `pct` percent above it. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `pct` | `float` | | `ref` | `number?` | | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.liqmap.densityBelow {#data-liqmap-densitybelow} ```flowscope data.liqmap.densityBelow(pct: float, ref: number? = null, barsAgo: int = 0) -> float? ``` Returns the notional below a price within a distance. The notional of the levels below `ref` down to `pct` percent under it. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `pct` | `float` | | `ref` | `number?` | | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.liqmap.clusterAbove {#data-liqmap-clusterabove} ```flowscope data.liqmap.clusterAbove(pct: float, minSize: float = 0.0, nearest: bool = true, ref: number? = null, barsAgo: int = 0) -> PriceBin? ``` Finds the nearest cluster above a price over a size. The nearest heatmap cell above `ref` (its centre at most `pct` percent away) holding at least `minSize` of notional; with `nearest: false`, the largest. The clusters a move is drawn to and where the forced orders fire. Cells are the heatmap's: the levels summed in price bins of `cellSize()`, starting at whole multiples of it, as the Heatmap draws one cell per bar and bin. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `pct` | `float` | | `minSize` | `float` | | `nearest` | `bool` | | `ref` | `number?` | | `barsAgo` | `int` | **Returns** `PriceBin?` **Available in** Handlers and functions called from handlers. ### data.liqmap.clusterBelow {#data-liqmap-clusterbelow} ```flowscope data.liqmap.clusterBelow(pct: float, minSize: float = 0.0, nearest: bool = true, ref: number? = null, barsAgo: int = 0) -> PriceBin? ``` Finds the nearest cluster below a price over a size. As `clusterAbove`, below `ref`. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `pct` | `float` | | `minSize` | `float` | | `nearest` | `bool` | | `ref` | `number?` | | `barsAgo` | `int` | **Returns** `PriceBin?` **Available in** Handlers and functions called from handlers. ### data.liqmap.range {#data-liqmap-range} ```flowscope data.liqmap.range(from: float, to: float, barsAgo: int = 0) -> LiqDensity? ``` Sums the levels in a price range. The notional of the levels from `from` to `to`, split at the period's price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `from` | `float` | | `to` | `float` | | `barsAgo` | `int` | **Returns** `LiqDensity?` **Available in** Handlers and functions called from handlers. ### data.liqmap.change {#data-liqmap-change} ```flowscope data.liqmap.change(price: float, bars: int = 1, barsAgo: int = 0) -> BookChange? ``` Compares a cell's notional between two periods. As `data.book.change`: whether a cluster was swept (gone) or grew. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `price` | `float` | | `bars` | `int` | | `barsAgo` | `int` | **Returns** `BookChange?` **Available in** Handlers and functions called from handlers. ### data.liqmap.price {#data-liqmap-price} ```flowscope data.liqmap.price(barsAgo: int = 0) -> float? ``` Returns the market's price at the end of a period. The close the period's levels are split at and the queries measure from. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.liqmap.cellSize {#data-liqmap-cellsize} ```flowscope data.liqmap.cellSize(barsAgo: int = 0) -> float? ``` Returns the heatmap's price bin of a period. As `data.book.cellSize`. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.liqmap.binSize {#data-liqmap-binsize} ```flowscope data.liqmap.binSize(barsAgo: int = 0) -> float? ``` Returns the map's recorded price bucket. The height of the map's own price buckets. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.liqmap.time {#data-liqmap-time} ```flowscope data.liqmap.time(barsAgo: int = 0) -> time? ``` Returns the start of a period. The opening time of the period read. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. A Flowscope extension. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `time?` **Available in** Handlers and functions called from handlers. ### data.cvd.change {#data-cvd-change} ```flowscope data.cvd.change() -> float? ``` Returns the change from open to close on the confirmed bar. Calculates `close - open` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.cvd.range {#data-cvd-range} ```flowscope data.cvd.range() -> float? ``` Returns the high-to-low range of the confirmed bar. Calculates `high - low` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.ohlcv.change {#data-ohlcv-change} ```flowscope data.ohlcv.change() -> float? ``` Returns the change from open to close on the confirmed bar. Calculates `close - open` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.ohlcv.delta {#data-ohlcv-delta} ```flowscope data.ohlcv.delta() -> float? ``` Returns the current OHLCV buy-sell volume delta. Calculates `buyVolume - sellVolume` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.ohlcv.range {#data-ohlcv-range} ```flowscope data.ohlcv.range() -> float? ``` Returns the high-to-low range of the confirmed bar. Calculates `high - low` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.ohlcv.tradeDelta {#data-ohlcv-tradedelta} ```flowscope data.ohlcv.tradeDelta() -> int? ``` Returns the current OHLCV buy-sell trade-count delta. Calculates `buyCount - sellCount` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `int?` **Available in** Handlers and functions called from handlers. ### data.oi.change {#data-oi-change} ```flowscope data.oi.change() -> float? ``` Returns the change from open to close on the confirmed bar. Calculates `close - open` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.oi.range {#data-oi-range} ```flowscope data.oi.range() -> float? ``` Returns the high-to-low range of the confirmed bar. Calculates `high - low` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.profile.buckets {#data-profile-buckets} ```flowscope data.profile.buckets(count: int, from: number? = null, to: number? = null) -> VolumeBuckets? ``` Groups volume into equal-width price buckets. `count` chooses the number of buckets. They span from the lowest selected price through one `binSize` above the highest. Each level goes into the bucket containing its price, so bucket totals add up to the selected range's totals. A `count` below 1 or above 4096 returns null. `from` and `to` select the price range first, including both ends. Null leaves that end unrestricted. An empty range or reversed bounds returns null. A window with no snapshots returns null. The profile combines volume at each price before grouping it. Read the values you need before calling `buckets` or `bucketsByLevels` again during the same calculation step. A later call replaces the data read through earlier bucket views, even on another snapshot or profile. Request the buckets again when you need a fresh view. | Parameter | Type | |---|---| | `count` | `int` | | `from` | `number?` | | `to` | `number?` | **Returns** `VolumeBuckets?` **Available in** Handlers and functions called from handlers. ### data.profile.bucketsByLevels {#data-profile-bucketsbylevels} ```flowscope data.profile.bucketsByLevels(levelsPerBucket: int, from: number? = null, to: number? = null) -> VolumeBuckets? ``` Groups consecutive price levels into volume buckets. `levelsPerBucket` chooses how many levels to put in each bucket. The last bucket can contain fewer levels. Each bucket starts at its first level's price and ends one `binSize` above its last level. A value below 1 returns null. The result cannot exceed 4096 buckets; exceeding that limit returns null. `from` and `to` select the price range first, including both ends. Null leaves that end unrestricted. An empty range or reversed bounds returns null. A window with no snapshots returns null. The profile combines volume at each price before grouping it. Read the values you need before calling `buckets` or `bucketsByLevels` again during the same calculation step. A later call replaces the data read through earlier bucket views, even on another snapshot or profile. Request the buckets again when you need a fresh view. | Parameter | Type | |---|---| | `levelsPerBucket` | `int` | | `from` | `number?` | | `to` | `number?` | **Returns** `VolumeBuckets?` **Available in** Handlers and functions called from handlers. ### data.profile.delta {#data-profile-delta} ```flowscope data.profile.delta() -> float? ``` Returns the rolling window's volume delta (buy - sell). Calculates `buy - sell` across the rolling profile. This matches `summary().delta` when the summary is non-null. A window with no snapshots returns null. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.profile.poc {#data-profile-poc} ```flowscope data.profile.poc(basis: VolBasis = 0, from: number? = null, to: number? = null) -> VolumeLevel? ``` Returns the point of control: the level with the largest selected volume. `basis` chooses which volume to compare, defaulting to `volbasis.total`. `from` and `to` optionally limit the price range, including both ends; null leaves that end unrestricted. Ties use the lowest price. Returns the combined level's price, buy, sell, total, delta, and ratio. Its `index` is -1 because it does not belong to one snapshot. An empty range or a window with no snapshots returns null. | Parameter | Type | |---|---| | `basis` | `VolBasis` | | `from` | `number?` | | `to` | `number?` | **Returns** `VolumeLevel?` **Available in** Handlers and functions called from handlers. ### data.profile.ratio {#data-profile-ratio} ```flowscope data.profile.ratio() -> float? ``` Returns the rolling window's volume-delta ratio (delta / total). Calculates `delta / total` across the rolling profile. This matches `summary().ratio` when the summary is non-null. A window with no snapshots returns null. A zero total volume gives NaN. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.profile.summary {#data-profile-summary} ```flowscope data.profile.summary(from: number? = null, to: number? = null) -> VolumeSummary? ``` Summarizes a rolling profile, optionally within a price range. `from` and `to` select prices, including both ends. Null leaves that end unrestricted. An empty range, reversed bounds, or a window with no snapshots returns null. `time` is the start time of the newest snapshot period. `binSize` is the finest price-bin size in the window. `levels` counts distinct combined price levels within the selected range. | Parameter | Type | |---|---| | `from` | `number?` | | `to` | `number?` | **Returns** `VolumeSummary?` **Available in** Handlers and functions called from handlers. ### data.profile.total {#data-profile-total} ```flowscope data.profile.total() -> float? ``` Returns the rolling window's total (buy + sell) volume. Calculates `buy + sell` across the rolling profile. This matches `summary().total` when the summary is non-null. A window with no snapshots returns null. With one snapshot, `profile(bars: 1).total()` equals `vol.total()`. Unavailable profile data also returns null. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.profile.valueArea {#data-profile-valuearea} ```flowscope data.profile.valueArea(percent: float, basis: VolBasis = 0, from: number? = null, to: number? = null) -> VolumeArea? ``` Returns the low and high prices of a volume value area. `percent` sets how much of the selected volume the area should cover. `basis` chooses the volume to use, defaulting to `volbasis.total`. `from` and `to` optionally limit the price range; null leaves that end unrestricted. Starts at the point of control, then adds the neighboring level with more selected volume until the target percentage is reached. Ties at the point of control or during expansion choose the lower price. At `percent: 100.0`, the area covers the whole range. `lowPrice` and `highPrice` describe the area. The other fields (`buy`, `sell`, `total`, `delta`, `ratio`, and `levels`) summarize the entire queried price range. An empty range or a window with no snapshots returns null. | Parameter | Type | |---|---| | `percent` | `float` | | `basis` | `VolBasis` | | `from` | `number?` | | `to` | `number?` | **Returns** `VolumeArea?` **Available in** Handlers and functions called from handlers. ### data.stat.liqDelta {#data-stat-liqdelta} ```flowscope data.stat.liqDelta() -> float? ``` Returns the current buy-sell liquidation delta. Calculates `buyLiq - sellLiq` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.stat.liqTotal {#data-stat-liqtotal} ```flowscope data.stat.liqTotal() -> float? ``` Returns the current total liquidation value. Calculates `buyLiq + sellLiq` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.vd.change {#data-vd-change} ```flowscope data.vd.change() -> float? ``` Returns the change from open to close on the confirmed bar. Calculates `close - open` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.vd.range {#data-vd-range} ```flowscope data.vd.range() -> float? ``` Returns the high-to-low range of the confirmed bar. Calculates `high - low` for this subscription's current confirmed bar. Returns null if the bar or either field is unavailable. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.volume.binSize {#data-volume-binsize} ```flowscope data.volume.binSize(barsAgo: int = 0) -> float? ``` Returns the width of each price bin. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.volume.buckets {#data-volume-buckets} ```flowscope data.volume.buckets(count: int, from: number? = null, to: number? = null, barsAgo: int = 0) -> VolumeBuckets? ``` Groups volume into equal-width price buckets. `count` chooses the number of buckets. They span from the lowest selected price through one `binSize` above the highest. Each level goes into the bucket containing its price, so bucket totals add up to the selected range's totals. A `count` below 1 or above 4096 returns null. `from` and `to` select the price range first, including both ends. Null leaves that end unrestricted. An empty range or reversed bounds returns null. `barsAgo` chooses the snapshot; a missing snapshot returns null. Read the values you need before calling `buckets` or `bucketsByLevels` again during the same calculation step. A later call replaces the data read through earlier bucket views, even on another snapshot or profile. Request the buckets again when you need a fresh view. | Parameter | Type | |---|---| | `count` | `int` | | `from` | `number?` | | `to` | `number?` | | `barsAgo` | `int` | **Returns** `VolumeBuckets?` **Available in** Handlers and functions called from handlers. ### data.volume.bucketsByLevels {#data-volume-bucketsbylevels} ```flowscope data.volume.bucketsByLevels(levelsPerBucket: int, from: number? = null, to: number? = null, barsAgo: int = 0) -> VolumeBuckets? ``` Groups consecutive price levels into volume buckets. `levelsPerBucket` chooses how many levels to put in each bucket. The last bucket can contain fewer levels. Each bucket starts at its first level's price and ends one `binSize` above its last level. A value below 1 returns null. `from` and `to` select the price range first, including both ends. Null leaves that end unrestricted. An empty range or reversed bounds returns null. `barsAgo` chooses the snapshot; a missing snapshot returns null. Read the values you need before calling `buckets` or `bucketsByLevels` again during the same calculation step. A later call replaces the data read through earlier bucket views, even on another snapshot or profile. Request the buckets again when you need a fresh view. | Parameter | Type | |---|---| | `levelsPerBucket` | `int` | | `from` | `number?` | | `to` | `number?` | | `barsAgo` | `int` | **Returns** `VolumeBuckets?` **Available in** Handlers and functions called from handlers. ### data.volume.bucketsByStep {#data-volume-bucketsbystep} ```flowscope data.volume.bucketsByStep(step: float, from: number? = null, to: number? = null, barsAgo: int = 0) -> VolumeBuckets? ``` Groups volume into price buckets of a fixed size on a shared price grid. `step` is the height of every bucket. Buckets start at whole multiples of `step`, so a price falls in the same bucket on every snapshot: rows line up across bars. The first bucket holds the lowest selected price, the last the highest, and empty buckets between them are included with zero volume. A `step` that is zero, negative or not finite returns null, as does a range that needs more than 4096 buckets. `from` and `to` select the price range first, including both ends. Null leaves that end open. An empty range or reversed bounds return null. `barsAgo` chooses the snapshot; a missing snapshot returns null. The view's `time` is the start of the snapshot's period: the `time` to pass when drawing the buckets as cells on that period's bar. | Parameter | Type | |---|---| | `step` | `float` | | `from` | `number?` | | `to` | `number?` | | `barsAgo` | `int` | **Returns** `VolumeBuckets?` **Available in** Handlers and functions called from handlers. ### data.volume.delta {#data-volume-delta} ```flowscope data.volume.delta(barsAgo: int = 0) -> float? ``` Returns the buy volume minus sell volume. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. An empty snapshot returns 0.0. This matches `summary(barsAgo).delta` when the summary is non-null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.volume.level {#data-volume-level} ```flowscope data.volume.level(index: int, barsAgo: int = 0) -> VolumeLevel? ``` Reads a volume-profile level by index. `index` counts traded price levels from 0, starting with the lowest price. `barsAgo` chooses the snapshot. Adjacent levels can be more than one `binSize` apart because prices with no stored trades are skipped. An index beyond the level count, unavailable history, or a missing snapshot returns null. Read the count with `summary(barsAgo).levels`. | Parameter | Type | |---|---| | `index` | `int` | | `barsAgo` | `int` | **Returns** `VolumeLevel?` **Available in** Handlers and functions called from handlers. ### data.volume.levels {#data-volume-levels} ```flowscope data.volume.levels(barsAgo: int = 0) -> VolumeLevels? ``` Returns volume-profile levels from lowest to highest price. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Contains the traded levels, matching `level(index, barsAgo:)` one by one. A snapshot with no levels returns an empty view with `len == 0`. These results include all profile levels; the order-book depth limit does not apply. Read the results in a `for` loop or a local `let` variable within the handler. They cannot be saved in `state`, record fields, or collections, passed to another function, or returned from one. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `VolumeLevels?` **Available in** Handlers and functions called from handlers. ### data.volume.numLevels {#data-volume-numlevels} ```flowscope data.volume.numLevels(barsAgo: int = 0) -> int? ``` Returns the number of traded price levels. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. An empty snapshot returns 0. This matches `summary(barsAgo).levels` when the summary is non-null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `int?` **Available in** Handlers and functions called from handlers. ### data.volume.poc {#data-volume-poc} ```flowscope data.volume.poc(basis: VolBasis = 0, from: number? = null, to: number? = null, barsAgo: int = 0) -> VolumeLevel? ``` Returns the point of control: the level with the largest selected volume. `basis` chooses which volume to compare, defaulting to `volbasis.total`. `from` and `to` optionally limit the price range, including both ends; null leaves that end unrestricted. Ties use the lowest price. `barsAgo` chooses the snapshot. An empty range, reversed bounds, or a missing snapshot returns null. | Parameter | Type | |---|---| | `basis` | `VolBasis` | | `from` | `number?` | | `to` | `number?` | | `barsAgo` | `int` | **Returns** `VolumeLevel?` **Available in** Handlers and functions called from handlers. ### data.volume.profile {#data-volume-profile} ```flowscope data.volume.profile(bars: int) -> Profile ``` Creates a rolling volume profile over the last `bars` confirmed snapshots. Returns a `Profile` combining the last `bars` completed volume snapshots available at the time the handler runs. The profile itself is never null, but its query results can be null. It has no `barsAgo:` parameter. Keep the profile in a `let` variable or call its methods directly. It cannot be saved in `state`, record fields, or collections, passed to a function, or returned from one. `bars` must be known before the script runs: use a literal, `input`, or `setup` value. Invalid `bars` values are rejected before the script runs. Each profile supports up to 65,536 distinct price levels and is also subject to the script's resource limits. If either limit is exceeded, its queries return null without stopping the script. Later queries can remain null even if the window becomes smaller. Refreshing the data or restarting the script allows another attempt; reduce `bars` if the limit keeps being reached. If fewer than `bars` completed snapshots are available, the profile uses those that exist. With no snapshots, all query methods return null. | Parameter | Type | |---|---| | `bars` | `int` | **Returns** `Profile` **Available in** Handlers and functions called from handlers. ### data.volume.ratio {#data-volume-ratio} ```flowscope data.volume.ratio(barsAgo: int = 0) -> float? ``` Returns the volume delta divided by total volume. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. Returns NaN if total volume is zero. This matches `summary(barsAgo).ratio` when the summary is non-null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.volume.summary {#data-volume-summary} ```flowscope data.volume.summary(barsAgo: int = 0, from: number? = null, to: number? = null) -> VolumeSummary? ``` Summarizes a volume profile, optionally within a price range. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. `from` and `to` select prices, including both ends. Null leaves that end unrestricted, so `summary(0)` reads the whole current profile. Reversed bounds or a range with no levels returns null. Read this from another subscription's handler. Volume profiles have no forming view. | Parameter | Type | |---|---| | `barsAgo` | `int` | | `from` | `number?` | | `to` | `number?` | **Returns** `VolumeSummary?` **Available in** Handlers and functions called from handlers. ### data.volume.time {#data-volume-time} ```flowscope data.volume.time(barsAgo: int = 0) -> time? ``` Returns the start time of the snapshot period. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `time?` **Available in** Handlers and functions called from handlers. ### data.volume.total {#data-volume-total} ```flowscope data.volume.total(barsAgo: int = 0) -> float? ``` Returns the total buy and sell volume. `barsAgo` counts periods on this subscription, with 0 selecting the current snapshot. An invalid position, a position outside available history, or a missing snapshot returns null. An empty snapshot returns 0.0. This matches `summary(barsAgo).total` when the summary is non-null. | Parameter | Type | |---|---| | `barsAgo` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### data.volume.valueArea {#data-volume-valuearea} ```flowscope data.volume.valueArea(percent: float, basis: VolBasis = 0, from: number? = null, to: number? = null, barsAgo: int = 0) -> VolumeArea? ``` Returns the low and high prices of a volume value area. `percent` sets how much of the selected volume the area should cover. `basis` chooses the volume to use, defaulting to `volbasis.total`. `from` and `to` optionally limit the price range; null leaves that end unrestricted. Starts at the point of control, then adds the neighboring level with more selected volume until the target percentage is reached. Ties at the point of control or during expansion choose the lower price. At `percent: 100.0`, the area covers the whole range. `lowPrice` and `highPrice` describe the area. The other fields (`buy`, `sell`, `total`, `delta`, `ratio`, and `levels`) summarize the entire queried price range. `barsAgo` chooses the snapshot. An empty range, reversed bounds, or a missing snapshot returns null. | Parameter | Type | |---|---| | `percent` | `float` | | `basis` | `VolBasis` | | `from` | `number?` | | `to` | `number?` | | `barsAgo` | `int` | **Returns** `VolumeArea?` **Available in** Handlers and functions called from handlers. ### data.volumeBuckets.get {#data-volumebuckets-get} ```flowscope data.volumeBuckets.get(index: int) -> FootprintBucket? ``` Reads a volume bucket by index. `index` counts from 0, starting with the lowest price range. A negative index or one at or beyond `len` returns null. `view[i]` is equivalent to `view.get(i)`. `total` is `buy + sell`; `delta` is `buy - sell`. A later `buckets` or `bucketsByLevels` call during the same calculation step replaces the data read through this view, even if it uses another snapshot or profile. | Parameter | Type | |---|---| | `index` | `int` | **Returns** `FootprintBucket?` **Available in** Handlers and functions called from handlers. ### data.volumeLevels.get {#data-volumelevels-get} ```flowscope data.volumeLevels.get(index: int) -> VolumeLevel? ``` Reads a volume-profile level by index. `index` counts from 0, starting with the lowest price. A negative index or one at or beyond `len` returns null. `view[i]` is equivalent to `view.get(i)`. The fields match `level(index, barsAgo:)` for the same snapshot. The level's `ratio` is NaN when its total volume is zero. | Parameter | Type | |---|---| | `index` | `int` | **Returns** `VolumeLevel?` **Available in** Handlers and functions called from handlers. ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `bookunit.base` | `BookUnit` | `1` | Selects base quantity for book imbalance calculations. | | `bookunit.quote` | `BookUnit` | `2` | Selects quote value for book imbalance calculations. | | `liqmap.model` | `LiqMap` | `0` | The liquidation model of the Liquidation Heatmap (default). | | `liqmap.hlLiquidations` | `LiqMap` | `1` | Hyperliquid: liquidation prices of tracked positions. | | `liqmap.hlStops` | `LiqMap` | `2` | Hyperliquid: stop-loss orders of tracked positions. | | `liqmap.hlTakeProfits` | `LiqMap` | `3` | Hyperliquid: take-profit orders of tracked positions. | | `volbasis.buy` | `VolBasis` | `1` | Ranks profile levels by buy volume. | | `volbasis.deltaAbs` | `VolBasis` | `3` | Ranks profile levels by \|buy - sell\| (absolute per-level delta). | | `volbasis.sell` | `VolBasis` | `2` | Ranks profile levels by sell volume. | | `volbasis.total` | `VolBasis` | `0` | Ranks profile levels by total (buy + sell) volume. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/data --- # markets reference > Facts about exchanges and symbols. Look up exchange categories, listed symbols, base and quote assets, tick and step sizes, and build combined exchange selections. ## Functions ### markets.agg {#markets-agg} ```flowscope markets.agg(exchanges: string...) -> string ``` Builds the combined exchange selection. Combines exchange names into a value you can pass to `subscribe(exchange:)`: `markets.agg("binance", "coinbase")` returns `"binance+coinbase"`. The order is kept: combined candles take their prices from the first exchange that traded in the bar. Exchange names are checked when you subscribe. A string list, such as the value of `input.exchanges`, can be passed to `exchange:` directly. You can call this during setup. Changing an input used to choose the exchanges restarts the script with the new selection. Pass each exchange name as a separate argument. | Parameter | Type | |---|---| | `exchanges` | `string` | **Returns** `string` ### markets.exchangeCategory {#markets-exchangecategory} ```flowscope markets.exchangeCategory(exchange: string) -> string ``` Returns an exchange's category, such as spot or futures. `exchange` selects the exchange. An unknown name returns an empty string. Use `markets.hasExchange` to check a name before reading its category. | Parameter | Type | |---|---| | `exchange` | `string` | **Returns** `string` **Available in** Handlers and functions called from handlers. ### markets.exchangeNames {#markets-exchangenames} ```flowscope markets.exchangeNames(category: string? = null) -> string[] ``` Lists exchange names, optionally filtered by category. `category` filters the list, for example to `"spot"` or `"futures"`. Omit it or use null for all exchanges. Names are sorted alphabetically. An unknown category returns an empty list. The returned list is read-only. Call this inside a handler. | Parameter | Type | |---|---| | `category` | `string?` | **Returns** `string[]` **Available in** Handlers and functions called from handlers. ### markets.exchangesForSymbol {#markets-exchangesforsymbol} ```flowscope markets.exchangesForSymbol(symbol: string) -> string[] ``` Lists the exchanges that offer a symbol. `symbol` selects the symbol. Exchange names are sorted alphabetically. An unknown symbol returns an empty list. The returned list is read-only. Call this inside a handler. | Parameter | Type | |---|---| | `symbol` | `string` | **Returns** `string[]` **Available in** Handlers and functions called from handlers. ### markets.hasExchange {#markets-hasexchange} ```flowscope markets.hasExchange(exchange: string) -> bool ``` Reports whether an exchange name is known. `exchange` must be one exchange name. A combined selection such as `"binancef:bybitf"` returns false. The result is never null and does not depend on live market updates. | Parameter | Type | |---|---| | `exchange` | `string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### markets.hasSymbol {#markets-hassymbol} ```flowscope markets.hasSymbol(exchange: string, symbol: string) -> bool ``` Reports whether a symbol is listed on an exchange. Checks whether `exchange` lists `symbol`. For a combined exchange selection, returns true if any member lists the symbol. If any member exchange is unknown, returns false. The result is never null and does not depend on the current bar or snapshot. | Parameter | Type | |---|---| | `exchange` | `string` | | `symbol` | `string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### markets.stepSize {#markets-stepsize} ```flowscope markets.stepSize(exchange: string, symbol: string) -> float? ``` Returns the smallest quantity increment for a symbol. `exchange` and `symbol` select the listing. An unknown listing or unavailable increment returns null. For a combined exchange selection from `markets.agg`, returns the smallest available increment among exchanges that list the symbol. Exchanges without the symbol are skipped. If any exchange is unknown, none lists the symbol, or no increment is available, returns null. | Parameter | Type | |---|---| | `exchange` | `string` | | `symbol` | `string` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### markets.symbolBase {#markets-symbolbase} ```flowscope markets.symbolBase(exchange: string, symbol: string) -> string ``` Returns the base asset of a symbol. `exchange` and `symbol` select the listing. An unknown exchange or symbol returns an empty string. The result does not depend on the current bar. For a combined exchange selection from `markets.agg`, reads the first exchange in alphabetical order that lists the symbol. If any exchange is unknown, or none lists the symbol, returns an empty string. | Parameter | Type | |---|---| | `exchange` | `string` | | `symbol` | `string` | **Returns** `string` **Available in** Handlers and functions called from handlers. ### markets.symbolNames {#markets-symbolnames} ```flowscope markets.symbolNames(exchange: string) -> string[] ``` Lists the symbols available on an exchange. `exchange` selects the exchange. Names are sorted alphabetically. An unknown exchange returns an empty list. The list is read-only and does not depend on the current bar or snapshot. | Parameter | Type | |---|---| | `exchange` | `string` | **Returns** `string[]` **Available in** Handlers and functions called from handlers. ### markets.symbolQuote {#markets-symbolquote} ```flowscope markets.symbolQuote(exchange: string, symbol: string) -> string ``` Returns the quote asset of a symbol. `exchange` and `symbol` select the listing. An unknown exchange or symbol returns an empty string. The result does not depend on the current bar. For a combined exchange selection from `markets.agg`, reads the first exchange in alphabetical order that lists the symbol. If any exchange is unknown, or none lists the symbol, returns an empty string. | Parameter | Type | |---|---| | `exchange` | `string` | | `symbol` | `string` | **Returns** `string` **Available in** Handlers and functions called from handlers. ### markets.tickSize {#markets-ticksize} ```flowscope markets.tickSize(exchange: string, symbol: string) -> float? ``` Returns the smallest price increment for a symbol. `exchange` and `symbol` select the listing. An unknown listing or unavailable increment returns null. For a combined exchange selection from `markets.agg`, returns the smallest available increment among exchanges that list the symbol. Exchanges without the symbol are skipped. If any exchange is unknown, none lists the symbol, or no increment is available, returns null. | Parameter | Type | |---|---| | `exchange` | `string` | | `symbol` | `string` | **Returns** `float?` **Available in** Handlers and functions called from handlers. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/markets --- # context reference > The chart the script runs on. `context.*` describes the chart's market and timeframe. The values are fixed for a run and readable everywhere, including `setup`. ## Properties ### context.exchange {#context-exchange} ```flowscope context.exchange: string ``` Exchange id of the chart, e.g. `binancef`. **Returns** `string` ### context.symbol {#context-symbol} ```flowscope context.symbol: string ``` Symbol of the chart, e.g. `BTCUSDT`. **Returns** `string` ### context.base {#context-base} ```flowscope context.base: string ``` Base asset of the chart's market. **Returns** `string` ### context.quote {#context-quote} ```flowscope context.quote: string ``` Quote asset of the chart's market. **Returns** `string` ### context.timeframe {#context-timeframe} ```flowscope context.timeframe: timeframe ``` Timeframe of the chart. **Returns** `timeframe` ### context.tickSize {#context-ticksize} ```flowscope context.tickSize: float ``` Price increment of the chart's market. **Returns** `float` ### context.stepSize {#context-stepsize} ```flowscope context.stepSize: float ``` Quantity increment of the chart's market. **Returns** `float` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/context --- # events reference > Handler events that run script code when data arrives. Code runs in `on` handlers: `on chart.open`, `on chart.update`, `on chart.close`, and `on trades.trade` for trade subscriptions. History is replayed through the same handlers before live data, so a script behaves the same on past and present bars. ## Events ### close {#close} ```flowscope close ``` Runs once when a bar closes and becomes confirmed. The bar values are final and the bar is now available in history. ### open {#open} ```flowscope open ``` Runs once when a new bar opens. Runs before any updates for the new bar. The previous bar is already confirmed and available in history. ### trade {#trade} ```flowscope trade ``` Runs once for each trade delivered by the subscription. The handler argument contains the trade's price, size, side, and time. ### update {#update} ```flowscope update ``` Runs on each update while the current bar is forming. Runs after the bar opens and before it closes. The forming values include the latest update. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/events --- # records reference > Records produced by subscriptions and snapshot queries. A subscription is a record whose fields are series: `chart.close` is the current value and `chart.close[1]` the previous one. Market fields are nullable; a missing value plots as a gap. ## Records ### BookDepth {#bookdepth} ```flowscope type BookDepth ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Book price-bin size. | | `bidQty` | `float` | — | Bid quantity in the query range. | | `askQty` | `float` | — | Ask quantity in the query range. | | `bidValue` | `float` | — | Bid quote value in the query range. | | `askValue` | `float` | — | Ask quote value in the query range. | | `bidLevels` | `int` | — | Bid levels included in the query range. | | `askLevels` | `int` | — | Ask levels included in the query range. | | `bidMinPrice` | `float?` | — | Lowest bid price included, or null when empty. | | `bidMaxPrice` | `float?` | — | Highest bid price included, or null when empty. | | `askMinPrice` | `float?` | — | Lowest ask price included, or null when empty. | | `askMaxPrice` | `float?` | — | Highest ask price included, or null when empty. | | `deltaQty` | `float` | — | Bid quantity minus ask quantity. | | `deltaValue` | `float` | — | Bid value minus ask value. | | `totalQty` | `float` | — | Bid plus ask quantity. | | `totalValue` | `float` | — | Bid plus ask value. | | `ratioQty` | `float` | — | Quantity imbalance ratio; NaN when total quantity is 0. | | `ratioValue` | `float` | — | Value imbalance ratio; NaN when total value is 0. | | `ratio` | `float` | — | Default quote-value imbalance ratio; NaN when total value is 0. | ### BookLevel {#booklevel} ```flowscope type BookLevel ``` | Field | Type | History | Description | |---|---|---|---| | `index` | `int` | — | 0-based level index within the side, 0 = best. | | `price` | `float` | — | Level price. | | `qty` | `float` | — | Level quantity. | | `value` | `float` | — | Total value at this level: `price * qty`. | ### BookLevels {#booklevels} ```flowscope type BookLevels ``` | Field | Type | History | Description | |---|---|---|---| | `len` | `int` | — | Number of levels in the view; raw depth may be capped by your data plan. | | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Book price-bin size. | ### LiqmapSub {#liqmapsub} ```flowscope type LiqmapSub ``` ### PriceBin {#pricebin} ```flowscope type PriceBin ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | The cell's height: the Heatmap's bin. | | `price` | `float` | — | The cell's lower edge. | | `top` | `float` | — | The cell's upper edge: `price + binSize`. | | `size` | `float` | — | What the cell holds: base units or quote value as asked (books), USD notional (level maps). | | `value` | `float` | — | Its quote value (notional). | | `distancePct` | `float` | — | The cell's centre from the reference, in percent: positive above, negative below. | ### BookChange {#bookchange} ```flowscope type BookChange ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the later period. | | `binSize` | `float` | — | The cell's height. | | `price` | `float` | — | The cell's lower edge. | | `top` | `float` | — | The cell's upper edge. | | `before` | `float` | — | Size in the earlier period. | | `after` | `float` | — | Size in the later period. | | `change` | `float` | — | `after - before`. | | `changePct` | `float` | — | The change in percent of `before`; NaN when `before` is 0. | ### LiqDensity {#liqdensity} ```flowscope type LiqDensity ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | The Heatmap's bin of the period. | | `ref` | `float` | — | The price the levels are split at. | | `below` | `float` | — | Notional of the levels below `ref`. | | `above` | `float` | — | Notional of the levels at or above `ref`. | | `total` | `float` | — | `below + above`. | | `delta` | `float` | — | `above - below`. | | `ratio` | `float` | — | `delta / total`; NaN when `total` is 0. | | `belowLevels` | `int` | — | Levels counted below. | | `aboveLevels` | `int` | — | Levels counted above. | ### BookSideSummary {#booksidesummary} ```flowscope type BookSideSummary ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Book price-bin size. | | `levels` | `int` | — | Levels included in the band. | | `qty` | `float` | — | Total quantity in the band. | | `value` | `float` | — | Total quote value, summed as price times quantity in the band. | | `avgPrice` | `float` | — | Quantity-weighted average price of the band (ladder VWAP): value / qty. | | `minPrice` | `float` | — | Lowest level price included. | | `maxPrice` | `float` | — | Highest level price included. | ### BookSub {#booksub} ```flowscope type BookSub ``` ### CvdForming {#cvdforming} ```flowscope type CvdForming ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | — | Opening time of the forming bar. | | `open` | `float?` | — | Forming cumulative volume-delta open value. | | `high` | `float?` | — | Forming cumulative volume-delta high value. | | `low` | `float?` | — | Forming cumulative volume-delta low value. | | `close` | `float?` | — | Forming cumulative volume-delta close value. | | `bucket` | `int` | — | CVD bucket group. | | `bucketLabel` | `string` | — | CVD bucket label. | | `bucketMinUsd` | `float?` | — | CVD bucket minimum USD notional. | | `bucketMaxUsd` | `float?` | — | CVD bucket maximum USD notional. | ### CvdSub {#cvdsub} ```flowscope type CvdSub ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | `[n]` | Opening time of the confirmed bar. | | `open` | `float?` | `[n]` | Confirmed cumulative volume-delta open value. | | `high` | `float?` | `[n]` | Confirmed cumulative volume-delta high value. | | `low` | `float?` | `[n]` | Confirmed cumulative volume-delta low value. | | `close` | `float?` | `[n]` | Confirmed cumulative volume-delta close value. | | `bucket` | `int` | — | CVD bucket group. | | `bucketLabel` | `string` | — | CVD bucket label. | | `bucketMinUsd` | `float?` | — | CVD bucket minimum USD notional. | | `bucketMaxUsd` | `float?` | — | CVD bucket maximum USD notional. | | `forming` | `CvdForming` | — | Current forming CVD values. Read these values directly; history indexing with `[n]` is not supported. | | `isNew` | `bool` | — | True during an update in which this subscription confirmed at least one period. | | `isRealtime` | `bool` | — | True while the forming period receives live updates. | | `isHistory` | `bool` | — | True during history replay. | | `isFirst` | `bool` | — | True when `index == 0`. | | `index` | `int` | — | Position of the current reference period in the loaded history, counting from 0. | | `isLast` | `bool` | — | True during live updates of the last loaded period, which is still forming. Always false during historical replay and inside `on X.close`, including the transition to live data. Use it in `on X.update` to draw only at the live edge. | ### FootprintBucket {#footprintbucket} ```flowscope type FootprintBucket ``` | Field | Type | History | Description | |---|---|---|---| | `index` | `int` | — | 0-based bucket index, ascending by price. | | `from` | `float` | — | Inclusive lower price bound of the bucket. | | `to` | `float` | — | Exclusive upper price bound of the bucket. | | `buy` | `float` | — | Buy volume re-binned into the bucket. | | `sell` | `float` | — | Sell volume re-binned into the bucket. | | `total` | `float` | — | Total volume: `buy + sell`. | | `delta` | `float` | — | Volume delta: `buy - sell`. | ### OhlcvForming {#ohlcvforming} ```flowscope type OhlcvForming ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | — | Opening time of the forming bar. | | `open` | `float?` | — | Forming open price. | | `high` | `float?` | — | Forming high price. | | `low` | `float?` | — | Forming low price. | | `close` | `float?` | — | Forming close price. | | `volume` | `float?` | — | Forming total volume. | | `buyVolume` | `float?` | — | Forming buy volume. | | `sellVolume` | `float?` | — | Forming sell volume. | | `buyCount` | `int?` | — | Forming buy trade count. | | `sellCount` | `int?` | — | Forming sell trade count. | | `trades` | `int?` | — | Forming trade count. | ### OhlcvSub {#ohlcvsub} ```flowscope type OhlcvSub ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | `[n]` | Opening time of the confirmed bar. `[n]` looks back by this subscription's periods. | | `open` | `float?` | `[n]` | Confirmed open price. | | `high` | `float?` | `[n]` | Confirmed high price. | | `low` | `float?` | `[n]` | Confirmed low price. | | `close` | `float?` | `[n]` | Confirmed close price. | | `volume` | `float?` | `[n]` | Confirmed total volume. | | `buyVolume` | `float?` | `[n]` | Confirmed buy volume. | | `sellVolume` | `float?` | `[n]` | Confirmed sell volume. | | `buyCount` | `int?` | `[n]` | Confirmed buy trade count. | | `sellCount` | `int?` | `[n]` | Confirmed sell trade count. | | `trades` | `int?` | `[n]` | Confirmed trade count. | | `forming` | `OhlcvForming` | — | Current forming OHLCV values. Read these values directly; history indexing with `[n]` is not supported. | | `isNew` | `bool` | — | True during an update in which this subscription confirmed at least one period. | | `isRealtime` | `bool` | — | True while the forming period receives live updates. | | `isHistory` | `bool` | — | True during history replay. | | `isFirst` | `bool` | — | True when `index == 0`. | | `index` | `int` | — | Position of the current reference period in the loaded history, counting from 0. | | `isLast` | `bool` | — | True during live updates of the last loaded period, which is still forming. Always false during historical replay and inside `on X.close`, including the transition to live data. Use it in `on X.update` to draw only at the live edge. | ### OiForming {#oiforming} ```flowscope type OiForming ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | — | Opening time of the forming bar. | | `open` | `float?` | — | Forming open-interest open value. | | `high` | `float?` | — | Forming open-interest high value. | | `low` | `float?` | — | Forming open-interest low value. | | `close` | `float?` | — | Forming open-interest close value. | ### OiSub {#oisub} ```flowscope type OiSub ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | `[n]` | Opening time of the confirmed bar. | | `open` | `float?` | `[n]` | Confirmed open-interest open value. | | `high` | `float?` | `[n]` | Confirmed open-interest high value. | | `low` | `float?` | `[n]` | Confirmed open-interest low value. | | `close` | `float?` | `[n]` | Confirmed open-interest close value. | | `forming` | `OiForming` | — | Current forming open-interest values. Read these values directly; history indexing with `[n]` is not supported. | | `isNew` | `bool` | — | True during an update in which this subscription confirmed at least one period. | | `isRealtime` | `bool` | — | True while the forming period receives live updates. | | `isHistory` | `bool` | — | True during history replay. | | `isFirst` | `bool` | — | True when `index == 0`. | | `index` | `int` | — | Position of the current reference period in the loaded history, counting from 0. | | `isLast` | `bool` | — | True during live updates of the last loaded period, which is still forming. Always false during historical replay and inside `on X.close`, including the transition to live data. Use it in `on X.update` to draw only at the live edge. | ### Profile {#profile} ```flowscope type Profile ``` ### StatForming {#statforming} ```flowscope type StatForming ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | — | Opening time of the forming statistics bar. | | `sellLiq` | `float?` | — | Last-value sell liquidation value. | | `buyLiq` | `float?` | — | Last-value buy liquidation value. | | `markPrice` | `float?` | — | Last-value mark price. Null when the feed carries no mark; aggregate exchange specs never have one. | | `fundingRate` | `float?` | — | Last-value funding rate. | ### StatSub {#statsub} ```flowscope type StatSub ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | `[n]` | Opening time of the confirmed statistics bar. | | `sellLiq` | `float?` | `[n]` | Confirmed sell liquidation value. | | `buyLiq` | `float?` | `[n]` | Confirmed buy liquidation value. | | `markPrice` | `float?` | `[n]` | Confirmed mark price. Null when the feed carries no mark; aggregate exchange specs never have one. | | `fundingRate` | `float?` | `[n]` | Confirmed funding rate. | | `forming` | `StatForming` | — | Current last-value statistics view. Read these values directly; history indexing with `[n]` is not supported. | | `isNew` | `bool` | — | True during an update in which this subscription confirmed at least one period. | | `isRealtime` | `bool` | — | True while the forming period receives live updates. | | `isHistory` | `bool` | — | True during history replay. | | `isFirst` | `bool` | — | True when `index == 0`. | | `index` | `int` | — | Position of the current reference period in the loaded history, counting from 0. | | `isLast` | `bool` | — | True during live updates of the last loaded period, which is still forming. Always false during historical replay and inside `on X.close`, including the transition to live data. Use it in `on X.update` to draw only at the live edge. | ### Trade {#trade} ```flowscope type Trade ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Trade timestamp in Unix seconds, rounded down to a whole second. | | `price` | `float` | — | Trade price. | | `size` | `float` | — | Trade size, always positive. | | `isBuy` | `bool` | — | true = aggressive buy, false = aggressive sell. | ### TradesSub {#tradessub} ```flowscope type TradesSub ``` ### VdForming {#vdforming} ```flowscope type VdForming ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | — | Opening time of the forming bar. | | `open` | `float?` | — | Forming volume-delta open value. | | `high` | `float?` | — | Forming volume-delta high value. | | `low` | `float?` | — | Forming volume-delta low value. | | `close` | `float?` | — | Forming volume-delta close value. | | `bucket` | `int` | — | Volume-delta bucket group. | | `bucketLabel` | `string` | — | Volume-delta bucket label. | | `bucketMinUsd` | `float?` | — | Volume-delta bucket minimum USD notional. | | `bucketMaxUsd` | `float?` | — | Volume-delta bucket maximum USD notional. | ### VdSub {#vdsub} ```flowscope type VdSub ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time?` | `[n]` | Opening time of the confirmed bar. | | `open` | `float?` | `[n]` | Confirmed volume-delta open value. | | `high` | `float?` | `[n]` | Confirmed volume-delta high value. | | `low` | `float?` | `[n]` | Confirmed volume-delta low value. | | `close` | `float?` | `[n]` | Confirmed volume-delta close value. | | `bucket` | `int` | — | Volume-delta bucket group. | | `bucketLabel` | `string` | — | Volume-delta bucket label. | | `bucketMinUsd` | `float?` | — | Volume-delta bucket minimum USD notional. | | `bucketMaxUsd` | `float?` | — | Volume-delta bucket maximum USD notional. | | `forming` | `VdForming` | — | Current forming volume-delta values. Read these values directly; history indexing with `[n]` is not supported. | | `isNew` | `bool` | — | True during an update in which this subscription confirmed at least one period. | | `isRealtime` | `bool` | — | True while the forming period receives live updates. | | `isHistory` | `bool` | — | True during history replay. | | `isFirst` | `bool` | — | True when `index == 0`. | | `index` | `int` | — | Position of the current reference period in the loaded history, counting from 0. | | `isLast` | `bool` | — | True during live updates of the last loaded period, which is still forming. Always false during historical replay and inside `on X.close`, including the transition to live data. Use it in `on X.update` to draw only at the live edge. | ### VolumeArea {#volumearea} ```flowscope type VolumeArea ``` | Field | Type | History | Description | |---|---|---|---| | `pocPrice` | `float` | — | Price with the largest value for the selected basis. Ties use the lowest price. | | `lowPrice` | `float` | — | Value-area low (VAL). | | `highPrice` | `float` | — | Value-area high (VAH). | | `buy` | `float` | — | Buy volume over the queried band. | | `sell` | `float` | — | Sell volume over the queried band. | | `total` | `float` | — | Total volume over the queried band. | | `delta` | `float` | — | Buy minus sell volume over the queried band. | | `ratio` | `float` | — | Volume-delta ratio over the queried band (NaN when total is 0). | | `levels` | `int` | — | Distinct price levels in the queried band. | ### VolumeBuckets {#volumebuckets} ```flowscope type VolumeBuckets ``` | Field | Type | History | Description | |---|---|---|---| | `len` | `int` | — | Number of buckets in the view. | | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Finest source price-bin size of the re-binned snapshot. | ### VolumeLevel {#volumelevel} ```flowscope type VolumeLevel ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Volume-profile price-bin size. | | `index` | `int` | — | 0-based level index, ascending by price. | | `price` | `float` | — | Level price. | | `buy` | `float` | — | Buy volume at this level. | | `sell` | `float` | — | Sell volume at this level. | | `total` | `float` | — | Buy plus sell volume. | | `delta` | `float` | — | Buy volume minus sell volume. | | `ratio` | `float` | — | Volume-delta ratio; NaN when total volume is 0. | ### VolumeLevels {#volumelevels} ```flowscope type VolumeLevels ``` | Field | Type | History | Description | |---|---|---|---| | `len` | `int` | — | Number of stored profile levels in the view. | | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Volume-profile price-bin size. | ### VolumeSub {#volumesub} ```flowscope type VolumeSub ``` ### VolumeSummary {#volumesummary} ```flowscope type VolumeSummary ``` | Field | Type | History | Description | |---|---|---|---| | `time` | `time` | — | Start time of the snapshot period. | | `binSize` | `float` | — | Volume-profile price-bin size. | | `levels` | `int` | — | Number of profile levels included. | | `minPrice` | `float` | — | Lowest profile price included. | | `maxPrice` | `float` | — | Highest profile price included. | | `buy` | `float` | — | Buy volume. | | `sell` | `float` | — | Sell volume. | | `total` | `float` | — | Total volume. | | `delta` | `float` | — | Buy volume minus sell volume. | | `ratio` | `float` | — | Volume-delta ratio; NaN when total volume is 0. | ### Macd {#macd} ```flowscope type Macd ``` | Field | Type | History | Description | |---|---|---|---| | `macd` | `float?` | — | The fast EMA minus the slow EMA. | | `signal` | `float?` | — | The signal EMA of the MACD line. | | `hist` | `float?` | — | MACD minus signal. | ### Bands {#bands} ```flowscope type Bands ``` | Field | Type | History | Description | |---|---|---|---| | `upper` | `float?` | — | The upper band. | | `middle` | `float?` | — | The middle line. | | `lower` | `float?` | — | The lower band. | ### Dmi {#dmi} ```flowscope type Dmi ``` | Field | Type | History | Description | |---|---|---|---| | `plus` | `float?` | — | +DI, 0 to 100. | | `minus` | `float?` | — | −DI, 0 to 100. | | `adx` | `float?` | — | The average directional index, 0 to 100. | ### SuperTrend {#supertrend} ```flowscope type SuperTrend ``` | Field | Type | History | Description | |---|---|---|---| | `line` | `float?` | — | The stop line. | | `direction` | `int?` | — | 1 in an uptrend, −1 in a downtrend. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/records --- # ta reference > Technical analysis: averages, oscillators, volatility, crosses and pivots. `ta.*` functions keep history per call site, advancing once per confirmed bar of the handler's subscription. Call them unconditionally in handlers and use the result conditionally. They return null during warm-up. ## Functions ### ta.alma {#ta-alma} ```flowscope ta.alma(source: float?, len: int, offset: float, sigma: float) -> float? ``` Returns the Arnaud Legoux moving average of the last `len` valid samples. `source` supplies one sample each time the calculation runs. `len` sets the valid-sample window; values outside its allowed range stop the script with an error. Sample `i` (0 = oldest, `len - 1` = newest) carries Gaussian weight `exp(-(i - m)^2 / (2 * s^2))` with `m = offset * (len - 1)` and `s = len / sigma`; the result is the weight-normalized sum (weights divided by their total). Null `source` samples output null and do not enter the window. The result stays null until `len` valid samples are available. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | | `offset` | `float` | | `sigma` | `float` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.barsSince {#ta-barssince} ```flowscope ta.barsSince(condition: bool) -> int? ``` Returns the number of bars since `condition` was last true. `condition` is checked each time the calculation runs. A true bar returns 0, and later false bars increment the count. The result is null until the first true bar has been observed. | Parameter | Type | |---|---| | `condition` | `bool` | **Returns** `int?` **Available in** Handlers and functions called from handlers. ### ta.change {#ta-change} ```flowscope ta.change(source: float?, len: int = 1) -> float? ``` Returns the change from `source` to a prior valid sample. `source` supplies one sample each time the calculation runs. `len` sets how many valid samples to look back, defaults to 1, and values outside its allowed range stop the script with an error. Null `source` samples output null and do not advance the lookback. The result stays null until the referenced prior valid sample exists. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.cross {#ta-cross} ```flowscope ta.cross(left: float?, right: float?) -> bool ``` Returns true when `left` and `right` cross in either direction. `left` and `right` are compared each time the calculation runs. Returns false during warmup or when either operand is null. Null operands do not update the stored comparison pair. | Parameter | Type | |---|---| | `left` | `float?` | | `right` | `float?` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### ta.crossover {#ta-crossover} ```flowscope ta.crossover(left: float?, right: float?) -> bool ``` Returns true when `left` crosses above `right`. `left` and `right` are compared each time the calculation runs. Returns true when `left > right` now and `left <= right` on the previous valid comparison pair. Returns false during warmup or when either operand is null. Null operands do not update the stored comparison pair. | Parameter | Type | |---|---| | `left` | `float?` | | `right` | `float?` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### ta.crossunder {#ta-crossunder} ```flowscope ta.crossunder(left: float?, right: float?) -> bool ``` Returns true when `left` crosses below `right`. `left` and `right` are compared each time the calculation runs. Returns true when `left < right` now and `left >= right` on the previous valid comparison pair. Returns false during warmup or when either operand is null. Null operands do not update the stored comparison pair. | Parameter | Type | |---|---| | `left` | `float?` | | `right` | `float?` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### ta.cum {#ta-cum} ```flowscope ta.cum(source: float?, reset: bool = false) -> float? ``` Returns the cumulative sum of valid `source` samples. `source` supplies one sample each time the calculation runs. The first valid sample starts the running total. Null `source` samples output null and leave the running total unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `reset` | `bool` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.dev {#ta-dev} ```flowscope ta.dev(source: float?, len: int) -> float? ``` Returns the mean absolute deviation of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. This is the average absolute distance from the mean. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.ema {#ta-ema} ```flowscope ta.ema(source: float?, len: int) -> float? ``` Returns the exponential moving average. `source` supplies the next sample. `len` sets the smoothing weight to `2 / (len + 1)`; values outside its allowed range stop the script with an error. The first non-null sample starts the average and is returned immediately. A null `source` returns null and leaves the average unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.falling {#ta-falling} ```flowscope ta.falling(source: float?, len: int) -> bool ``` Reports whether `source` is below every earlier sample in the window. `source` supplies the next sample. `len` sets the number of samples, including the current one; values outside its allowed range stop the script with an error. Returns true when the current sample is strictly less than each of the `len - 1` earlier non-null samples. Returns false until enough samples are available or when `source` is null. Null samples do not enter the history. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### ta.highest {#ta-highest} ```flowscope ta.highest(source: float?, len: int) -> float? ``` Returns the highest value of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. Infinities compare normally. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.hma {#ta-hma} ```flowscope ta.hma(source: float?, len: int) -> float? ``` Returns the Hull moving average of `source`. `source` supplies one sample each time the calculation runs. `len` sets the weighted-average window; values outside its allowed range stop the script with an error. The calculation combines weighted moving averages and a final `sqrt(len)` smoothing window. The result stays null until `len + sqrt(len) - 1` valid samples are available; null `source` samples output null and do not advance the windows. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.linreg {#ta-linreg} ```flowscope ta.linreg(source: float?, len: int, offset: int = 0) -> float? ``` Returns a linear-regression value over the last `len` valid samples. `source` supplies one sample each time the calculation runs. `len` sets the valid-sample window; values outside its allowed range stop the script with an error. `offset` selects where the fitted line is evaluated and defaults to 0. Null `source` samples output null and do not enter the window. The result stays null until `len` valid samples are available. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | | `offset` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.lowest {#ta-lowest} ```flowscope ta.lowest(source: float?, len: int) -> float? ``` Returns the lowest value of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. Infinities compare normally. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.max {#ta-max} ```flowscope ta.max(source: float?, reset: bool = false) -> float? ``` Returns the running maximum of valid `source` samples. `source` supplies one sample each time the calculation runs. The first valid sample starts the running maximum. Null `source` samples output null and leave the running maximum unchanged. `reset` true forgets the running maximum before this bar's sample is compared, even when that sample is null. | Parameter | Type | |---|---| | `source` | `float?` | | `reset` | `bool` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.median {#ta-median} ```flowscope ta.median(source: float?, len: int) -> float? ``` Returns the median of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.min {#ta-min} ```flowscope ta.min(source: float?, reset: bool = false) -> float? ``` Returns the running minimum of valid `source` samples. `source` supplies one sample each time the calculation runs. The first valid sample starts the running minimum. Null `source` samples output null and leave the running minimum unchanged. `reset` true forgets the running minimum before this bar's sample is compared, even when that sample is null. | Parameter | Type | |---|---| | `source` | `float?` | | `reset` | `bool` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.mode {#ta-mode} ```flowscope ta.mode(source: float?, len: int) -> float? ``` Returns the most frequent value of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. If several values tie, returns the smallest of them. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.pivotHigh {#ta-pivothigh} ```flowscope ta.pivotHigh(source: float?, left: int, right: int) -> float? ``` Returns a pivot high once later samples confirm it. `source` supplies the next sample. `left` sets how many earlier samples to check, and `right` sets how many later samples must confirm the pivot. Values outside either allowed range stop the script with an error. A sample qualifies when none of the other `left + right` samples is higher. Equal values do not disqualify it. The pivot is returned `right` non-null samples after it occurs, on the confirmation bar. Otherwise the result is null. A null `source` returns null and does not enter the sample history. | Parameter | Type | |---|---| | `source` | `float?` | | `left` | `int` | | `right` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.pivotLow {#ta-pivotlow} ```flowscope ta.pivotLow(source: float?, left: int, right: int) -> float? ``` Returns a pivot low once later samples confirm it. `source` supplies the next sample. `left` sets how many earlier samples to check, and `right` sets how many later samples must confirm the pivot. Values outside either allowed range stop the script with an error. A sample qualifies when none of the other `left + right` samples is lower. Equal values do not disqualify it. The pivot is returned `right` non-null samples after it occurs, on the confirmation bar. Otherwise the result is null. A null `source` returns null and does not enter the sample history. | Parameter | Type | |---|---| | `source` | `float?` | | `left` | `int` | | `right` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.rising {#ta-rising} ```flowscope ta.rising(source: float?, len: int) -> bool ``` Reports whether `source` is above every earlier sample in the window. `source` supplies the next sample. `len` sets the number of samples, including the current one; values outside its allowed range stop the script with an error. Returns true when the current sample is strictly greater than each of the `len - 1` earlier non-null samples. Returns false until enough samples are available or when `source` is null. Null samples do not enter the history. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### ta.rma {#ta-rma} ```flowscope ta.rma(source: float?, len: int) -> float? ``` Returns the Wilder's smoothed moving average. `source` supplies the next sample. `len` sets the smoothing weight to `1 / len`; values outside its allowed range stop the script with an error. The first non-null sample starts the average and is returned immediately. A null `source` returns null and leaves the average unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.roc {#ta-roc} ```flowscope ta.roc(source: float?, len: int) -> float? ``` Returns the rate of change from a prior valid sample. `source` supplies one sample each time the calculation runs. `len` sets how many valid samples to look back; values outside its allowed range stop the script with an error. Null `source` samples output null and do not advance the lookback. The result stays null until the referenced prior sample exists, and is null when that prior value is zero. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.rsi {#ta-rsi} ```flowscope ta.rsi(source: float?, len: int) -> float? ``` Returns the relative strength index over valid source changes. `source` supplies one sample each time the calculation runs. `len` sets the Wilder smoothing window; values outside its allowed range stop the script with an error. The result stays null until `len + 1` valid samples provide enough changes. Null `source` samples output null and leave the average gain and loss unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.sma {#ta-sma} ```flowscope ta.sma(source: float?, len: int) -> float? ``` Returns the simple moving average of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.stdev {#ta-stdev} ```flowscope ta.stdev(source: float?, len: int) -> float? ``` Returns the population standard deviation of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. The calculation divides by `len`. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.zscore {#ta-zscore} ```flowscope ta.zscore(source: float?, len: int) -> float? ``` Normalizes the current sample by its rolling population mean and standard deviation. Uses the last `len` finite samples, including the current sample. Returns null until the window is full; null or non-finite input leaves the window unchanged. Constant windows return zero. Length must be 1 through 10000. Useful for comparing volume, delta and price deviations across intervals. Forming updates do not change confirmed state. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.stoch {#ta-stoch} ```flowscope ta.stoch(source: float?, high: float?, low: float?, len: int) -> float? ``` Returns the stochastic position of `source` within the recent range. `source`, `high`, and `low` supply the current values. `len` sets how many non-null samples to include; values outside its allowed range stop the script with an error. Returns the percentage position of `source` between the lowest low and highest high in that window. If any input is null, returns null and does not add a sample. | Parameter | Type | |---|---| | `source` | `float?` | | `high` | `float?` | | `low` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.sum {#ta-sum} ```flowscope ta.sum(source: float?, len: int) -> float? ``` Returns the sum of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. Null samples do not count as zero. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.swma {#ta-swma} ```flowscope ta.swma(source: float?) -> float? ``` Returns a four-sample symmetrically weighted moving average. `source` supplies one sample each time the calculation runs. The fixed window uses four valid samples with symmetric weights 1, 2, 2, 1 (oldest to newest) divided by 6. Null `source` samples output null and do not enter the window. The result stays null until four valid samples are available. | Parameter | Type | |---|---| | `source` | `float?` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.variance {#ta-variance} ```flowscope ta.variance(source: float?, len: int) -> float? ``` Returns the population variance of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. The calculation divides by `len`. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.wma {#ta-wma} ```flowscope ta.wma(source: float?, len: int) -> float? ``` Returns the linearly weighted moving average of the last `len` non-null samples. `source` supplies the next sample. `len` chooses how many non-null samples to include; values outside its allowed range stop the script with an error. Newer samples receive larger weights. Returns null until `len` non-null samples are available. A null `source` returns null and leaves the sample history unchanged. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.macd {#ta-macd} ```flowscope ta.macd(source: float?, fast: int = 12, slow: int = 26, signal: int = 9) -> Macd ``` Returns the MACD line, its signal line and the histogram. The MACD line is the `fast` EMA of `source` minus the `slow` EMA; the signal line is the `signal` EMA of the MACD line, and `hist` is MACD minus signal. Every EMA is seeded with the mean of its first `len` samples, as `ta.ema`. Always returns a `Macd` record; a field is null until its average has its warm-up: `ta.macd(chart.close).hist ?? 0.0`. Null samples are skipped. | Parameter | Type | |---|---| | `source` | `float?` | | `fast` | `int` | | `slow` | `int` | | `signal` | `int` | **Returns** `Macd` **Available in** Handlers and functions called from handlers. ### ta.bb {#ta-bb} ```flowscope ta.bb(source: float?, len: int = 20, mult: float = 2.0) -> Bands ``` Returns Bollinger Bands: the SMA and `mult` standard deviations either side. `len` valid samples make the window; the deviation is the population standard deviation, as `ta.stdev`. `mult` must be positive. The `Bands` fields are null until `len` samples are in the window. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | | `mult` | `float` | **Returns** `Bands` **Available in** Handlers and functions called from handlers. ### ta.cci {#ta-cci} ```flowscope ta.cci(source: float?, len: int) -> float? ``` Returns the Commodity Channel Index. The distance of `source` from its `len`-sample mean in units of 0.015 mean absolute deviations. Usually given the typical price `(high + low + close) / 3`. Zero while the window is flat; null until `len` samples are in it. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.percentRank {#ta-percentrank} ```flowscope ta.percentRank(source: float?, len: int) -> float? ``` Returns the share of the previous `len` samples at or below the current one. In percent, 0 to 100. Null until `len + 1` valid samples have been seen; null samples are skipped. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.correlation {#ta-correlation} ```flowscope ta.correlation(a: float?, b: float?, len: int) -> float? ``` Returns the Pearson correlation of two series. Over the last `len` samples where both `a` and `b` are known, from −1 to 1. Null until the window is full and while either series is flat in it. | Parameter | Type | |---|---| | `a` | `float?` | | `b` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.valueWhen {#ta-valuewhen} ```flowscope ta.valueWhen(condition: bool, source: float?, occurrence: int = 0) -> float? ``` Returns `source` as it was when `condition` last held. `occurrence` counts back from the latest time the condition held (0) to earlier ones. Null until that many occurrences with a known `source` have been seen. | Parameter | Type | |---|---| | `condition` | `bool` | | `source` | `float?` | | `occurrence` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.cmo {#ta-cmo} ```flowscope ta.cmo(source: float?, len: int) -> float? ``` Returns the Chande Momentum Oscillator. The sum of rises minus the sum of falls over the last `len` changes, divided by their total, in percent (−100 to 100). Zero while nothing moved. | Parameter | Type | |---|---| | `source` | `float?` | | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.tsi {#ta-tsi} ```flowscope ta.tsi(source: float?, short: int = 13, long: int = 25) -> float? ``` Returns the True Strength Index. The change of `source` smoothed by a `long` EMA and then a `short` EMA, over its absolute value smoothed the same way: −1 to 1. Null until both smoothings have their warm-up. | Parameter | Type | |---|---| | `source` | `float?` | | `short` | `int` | | `long` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ## Methods ### ta.accDist {#ta-accdist} ```flowscope ta.accDist() -> float? ``` Returns the accumulation/distribution line for the subscription. Uses the subscription's high, low, close, and volume. The first non-null sample starts the cumulative line. A null input returns null and leaves the running total unchanged. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.atr {#ta-atr} ```flowscope ta.atr(len: int) -> float? ``` Returns the average true range over valid OHLCV samples. `len` sets Wilder smoothing; values outside its allowed range stop the script with an error. Uses the subscription's high, low, and close. True range uses the current high and low and the previous close. The first non-null sample returns a result immediately. A null input returns null and leaves the average unchanged. | Parameter | Type | |---|---| | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.mfi {#ta-mfi} ```flowscope ta.mfi(len: int) -> float? ``` Returns the money flow index over valid OHLCV samples. `len` sets the valid-sample window; values outside its allowed range stop the script with an error. The subscription supplies high, low, close, and volume. Null input fields make the bar output null and do not update the window. The result stays null until `len + 1` valid samples are available. | Parameter | Type | |---|---| | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.tr {#ta-tr} ```flowscope ta.tr() -> float? ``` Returns true range for the subscription's current bar. Uses the current high and low and the previous close. The first non-null sample returns a result immediately. A null input returns null and leaves the previous close unchanged. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.vwap {#ta-vwap} ```flowscope ta.vwap(reset: bool = false, source: float? = null) -> float? ``` Returns the volume-weighted average price since the last reset. The subscription supplies price and volume. - `reset` starts a new average before adding the current sample, even when that sample is null. The first valid sample starts the new average. - `source` optionally replaces the close price. Omitting it uses close; a supplied expression that returns null counts as a missing sample. A null input returns null and leaves the totals unchanged. The result is also null while accumulated volume is zero. A non-null reset sample with volume returns its own price. During a forming candle, resets are temporary and are recalculated on the next update. | Parameter | Type | |---|---| | `reset` | `bool` | | `source` | `float?` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.vwapDev {#ta-vwapdev} ```flowscope ta.vwapDev(reset: bool = false, source: float? = null) -> float? ``` Returns the volume-weighted standard deviation around VWAP. The subscription supplies price and volume. Use `vwap + mult * vwapDev` and `vwap - mult * vwapDev` for upper and lower bands. - `reset` starts new totals before adding the current sample, even when it is null. - `source` optionally replaces the close price. Omitting it uses close; a supplied expression that returns null counts as a missing sample. This is a population standard deviation over the bars since the last reset. The first valid sample with volume returns 0. A null input returns null and leaves the totals unchanged. The result is also null while accumulated volume is zero. Use the same `reset` and `source` as the matching `vwap` call so both cover the same bars. | Parameter | Type | |---|---| | `reset` | `bool` | | `source` | `float?` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.vwma {#ta-vwma} ```flowscope ta.vwma(len: int) -> float? ``` Returns a volume-weighted moving average over valid OHLCV samples. `len` sets the valid-sample window; values outside its allowed range stop the script with an error. The subscription supplies close and volume. Null input fields output null and do not enter the window. The result stays null until `len` valid samples are available. | Parameter | Type | |---|---| | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.kc {#ta-kc} ```flowscope ta.kc(len: int = 20, mult: float = 2.0) -> Bands ``` Returns Keltner Channels around the EMA of the close. Called on a subscription: `chart.kc()`. The middle line is the `len` EMA of the close; the channel lies `mult` EMAs of the true range either side. The `Bands` fields are null until both averages have their warm-up. | Parameter | Type | |---|---| | `len` | `int` | | `mult` | `float` | **Returns** `Bands` **Available in** Handlers and functions called from handlers. ### ta.wpr {#ta-wpr} ```flowscope ta.wpr(len: int = 14) -> float? ``` Returns Williams %R on the subscription's bars. Called on a subscription: `chart.wpr()`. The close's place in the range of the last `len` highs and lows, from −100 (at the low) to 0 (at the high). Null while the range is zero or not yet `len` bars long. | Parameter | Type | |---|---| | `len` | `int` | **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.obv {#ta-obv} ```flowscope ta.obv() -> float? ``` Returns on-balance volume. Called on an OHLCV subscription: `chart.obv()`. Adds the bar's volume when the close rose, subtracts it when it fell, and keeps the total when it is unchanged. The first bar starts at zero. **Returns** `float?` **Available in** Handlers and functions called from handlers. ### ta.dmi {#ta-dmi} ```flowscope ta.dmi(len: int = 14, adxLen: int = 14) -> Dmi ``` Returns Wilder's directional movement: +DI, −DI and the ADX. Called on a subscription: `chart.dmi()`. Directional movement and true range are Wilder-averaged over `len` bars; the ADX averages their normalized difference over `adxLen`. All values lie between 0 and 100. The `Dmi` fields are null until their averages have their warm-up. | Parameter | Type | |---|---| | `len` | `int` | | `adxLen` | `int` | **Returns** `Dmi` **Available in** Handlers and functions called from handlers. ### ta.supertrend {#ta-supertrend} ```flowscope ta.supertrend(factor: float = 3.0, len: int = 10) -> SuperTrend ``` Returns the SuperTrend line and its direction. Called on a subscription: `chart.supertrend()`. A band of `factor` ATRs (`len` bars) around the median price that only tightens in the trend's direction and flips side when the close crosses it. `direction` is 1 in an uptrend (the line under price) and −1 in a downtrend. The fields are null until the ATR has its warm-up. | Parameter | Type | |---|---| | `factor` | `float` | | `len` | `int` | **Returns** `SuperTrend` **Available in** Handlers and functions called from handlers. ### ta.sar {#ta-sar} ```flowscope ta.sar(start: float = 0.02, step: float = 0.02, max: float = 0.2) -> float? ``` Returns Wilder's Parabolic SAR. Called on a subscription: `chart.sar()`. The stop follows the trend's extreme with an acceleration that starts at `start`, grows by `step` with each new extreme up to `max`, never enters the two previous bars' range, and flips to the extreme when price crosses it. Null on the first bar. | Parameter | Type | |---|---| | `start` | `float` | | `step` | `float` | | `max` | `float` | **Returns** `float?` **Available in** Handlers and functions called from handlers. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/ta --- # math reference > Numeric helpers and constants. `math.*` functions are pure and work anywhere. A null argument gives a null result unless noted. ## Functions ### math.abs {#math-abs} ```flowscope math.abs(value: number) -> same_numeric ``` Returns the absolute value of a number. `value` keeps its type: an int returns an int and a float returns a float. Check for null or use `??` to supply a fallback before calling. Null float inputs produce null. | Parameter | Type | |---|---| | `value` | `number` | **Returns** `same_numeric` ### math.acos {#math-acos} ```flowscope math.acos(value: float) -> float? ``` Returns the arccosine of a float in radians. `value` must be in `[-1.0, 1.0]` for a real result. Values outside that range return null. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float?` ### math.asin {#math-asin} ```flowscope math.asin(value: float) -> float? ``` Returns the arcsine of a float in radians. `value` must be in `[-1.0, 1.0]` for a real result. Values outside that range return null. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float?` ### math.atan {#math-atan} ```flowscope math.atan(value: float) -> float ``` Returns the arctangent of a number in radians. The result is between `-math.pi / 2.0` and `math.pi / 2.0`. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.atan2 {#math-atan2} ```flowscope math.atan2(y: float, x: float) -> float ``` Returns the angle of a vector in radians. `y` and `x` are the vector's coordinates. The result is from `-math.pi` to `math.pi`, inclusive. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `y` | `float` | | `x` | `float` | **Returns** `float` ### math.cbrt {#math-cbrt} ```flowscope math.cbrt(value: float) -> float ``` Returns the cube root of a number. `value` can be negative, zero, or positive. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.ceil {#math-ceil} ```flowscope math.ceil(value: float) -> float ``` Rounds a number up to a whole number. For example, `1.2` becomes `2.0` and `-1.8` becomes `-1.0`. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.ceilTo {#math-ceilto} ```flowscope math.ceilTo(value: float, step: float) -> float? ``` Rounds a float up to a multiple of `step`. `value` is divided by `step`, rounded up, and multiplied by `step`. A zero, null, or NaN `step` returns null instead of stopping the script with an error. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | | `step` | `float` | **Returns** `float?` ### math.clamp {#math-clamp} ```flowscope math.clamp(value: number, min: number, max: number) -> same_numeric ``` Keeps a number within a range. Returns `value` when it is between `min` and `max`, including both ends. Below the range it returns `min`; above the range it returns `max`. Mixing ints and floats gives a float result. `min > max` stops the script with an error. Check for null or use `??` to supply a fallback before calling. | Parameter | Type | |---|---| | `value` | `number` | | `min` | `number` | | `max` | `number` | **Returns** `same_numeric` ### math.cos {#math-cos} ```flowscope math.cos(value: float) -> float ``` Returns the cosine of an angle in radians. `value` is the angle in radians. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.cosh {#math-cosh} ```flowscope math.cosh(value: float) -> float ``` Returns the hyperbolic cosine of a number. `value` is the number to calculate from. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.exp {#math-exp} ```flowscope math.exp(value: float) -> float ``` Returns e raised to a power. `value` is the exponent. A result too large for a float may be infinite. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.floor {#math-floor} ```flowscope math.floor(value: float) -> float ``` Rounds a number down to a whole number. For example, `1.8` becomes `1.0` and `-1.2` becomes `-2.0`. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.floorTo {#math-floorto} ```flowscope math.floorTo(value: float, step: float) -> float? ``` Rounds a float down to a multiple of `step`. `value` is divided by `step`, rounded down, and multiplied by `step`. A zero, null, or NaN `step` returns null instead of stopping the script with an error. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | | `step` | `float` | **Returns** `float?` ### math.isEven {#math-iseven} ```flowscope math.isEven(value: int) -> bool ``` Tests whether an integer is even. Returns true when `value % 2 == 0`. Check for null before using a nullable int. Supported integers range from `math.minInt` (-9223372036854775807) to `math.maxInt` (9223372036854775807). | Parameter | Type | |---|---| | `value` | `int` | **Returns** `bool` ### math.isFinite {#math-isfinite} ```flowscope math.isFinite(value: float?) -> bool ``` Reports whether a number is finite and non-null. Returns false for null, NaN, and positive or negative infinity; otherwise true. This check does not change a nullable value's type. Use `x != null` when you need to use `x` as a non-null value. | Parameter | Type | |---|---| | `value` | `float?` | **Returns** `bool` ### math.isOdd {#math-isodd} ```flowscope math.isOdd(value: int) -> bool ``` Tests whether an integer is odd. Returns true when `value % 2 != 0`. Check for null before using a nullable int. Supported integers range from `math.minInt` (-9223372036854775807) to `math.maxInt` (9223372036854775807). | Parameter | Type | |---|---| | `value` | `int` | **Returns** `bool` ### math.log {#math-log} ```flowscope math.log(value: float) -> float? ``` Returns the natural logarithm of a float. `value` must be greater than 0.0 for a real result. Values at or below 0.0 return null. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float?` ### math.log10 {#math-log10} ```flowscope math.log10(value: float) -> float? ``` Returns the base-10 logarithm of a float. `value` must be greater than 0.0 for a real result. Values at or below 0.0 return null. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float?` ### math.log2 {#math-log2} ```flowscope math.log2(value: float) -> float? ``` Returns the base-2 logarithm of a float. `value` must be greater than 0.0 for a real result. Values at or below 0.0 return null. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float?` ### math.max {#math-max} ```flowscope math.max(value: number, values: number...) -> same_numeric ``` Returns the largest of one or more numbers. Pass the first number as `value` and any others as `values`. Mixing ints and floats gives a float result. Check for null or use `??` to supply a fallback before calling. NaN inputs produce null. | Parameter | Type | |---|---| | `value` | `number` | | `values` | `number` | **Returns** `same_numeric` ### math.min {#math-min} ```flowscope math.min(value: number, values: number...) -> same_numeric ``` Returns the smallest of one or more numbers. Pass the first number as `value` and any others as `values`. Mixing ints and floats gives a float result. Check for null or use `??` to supply a fallback before calling. NaN inputs produce null. | Parameter | Type | |---|---| | `value` | `number` | | `values` | `number` | **Returns** `same_numeric` ### math.pow {#math-pow} ```flowscope math.pow(base: float, exponent: float) -> float? ``` Raises a number to a power. Returns `base` raised to `exponent`, as a float. Inputs with no real-number result, such as a negative base with a fractional exponent, return null. Check for null or use `??` to supply a fallback before calling. | Parameter | Type | |---|---| | `base` | `float` | | `exponent` | `float` | **Returns** `float?` ### math.randomFloat {#math-randomfloat} ```flowscope math.randomFloat(min: float, max: float) -> float ``` Returns a random float from `min` up to, but not including, `max`. Both bounds are required. Every equal-sized part of the range is equally likely. `min >= max` stops the script with an error. Call this inside a handler. Replaying the same script uses the same sequence of random values. | Parameter | Type | |---|---| | `min` | `float` | | `max` | `float` | **Returns** `float` **Available in** Handlers and functions called from handlers. ### math.randomInt {#math-randomint} ```flowscope math.randomInt(min: int, max: int) -> int ``` Returns a random whole number between `min` and `max`, including both. Both bounds are required. Every integer in the range is equally likely. `min > max` stops the script with an error. Bounds must be within `math.minInt` (-9223372036854775807) and `math.maxInt` (9223372036854775807). Replaying the same script uses the same sequence of random values. | Parameter | Type | |---|---| | `min` | `int` | | `max` | `int` | **Returns** `int` **Available in** Handlers and functions called from handlers. ### math.round {#math-round} ```flowscope math.round(value: number, decimals: int = 0) -> float ``` Rounds a number to a chosen number of decimal places. `value` can be an int or float; the result is always a float. `decimals` defaults to 0, which rounds to a whole number. Negative `decimals` round to tens, hundreds, and so on. Check for null or use `??` to supply a fallback before calling. NaN inputs return null. | Parameter | Type | |---|---| | `value` | `number` | | `decimals` | `int` | **Returns** `float` ### math.roundTo {#math-roundto} ```flowscope math.roundTo(value: float, step: float) -> float? ``` Rounds a float to the nearest multiple of `step`. `value` is divided by `step`, rounded to the nearest integer value, and multiplied by `step`. A zero, null, or NaN `step` returns null instead of stopping the script with an error. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | | `step` | `float` | **Returns** `float?` ### math.safeDiv {#math-safediv} ```flowscope math.safeDiv(num: float?, den: float?, fallback: float = 0) -> float? ``` Returns a division result with a fallback for zero or missing denominators. `num` is the numerator. `den` is the denominator; zero or null `den` returns `fallback`. Null `num` returns null because the numerator is missing. Otherwise the result is `num / den`. | Parameter | Type | |---|---| | `num` | `float?` | | `den` | `float?` | | `fallback` | `float` | **Returns** `float?` ### math.sign {#math-sign} ```flowscope math.sign(value: float) -> float ``` Returns `-1.0`, `0.0`, or `1.0` for a number's sign. Returns `-1.0` for negative values, `1.0` for positive values, and `0.0` for both `0.0` and `-0.0`. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.sin {#math-sin} ```flowscope math.sin(value: float) -> float ``` Returns the sine of an angle in radians. `value` is the angle in radians. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.sinh {#math-sinh} ```flowscope math.sinh(value: float) -> float ``` Returns the hyperbolic sine of a number. `value` is the number to calculate from. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.sqrt {#math-sqrt} ```flowscope math.sqrt(value: float) -> float? ``` Returns the square root of a float. `value` must be non-negative for a real result. Negative values produce null. Check for null before calling. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float?` ### math.tan {#math-tan} ```flowscope math.tan(value: float) -> float ``` Returns the tangent of an angle in radians. `value` is the angle in radians. Near odd multiples of `math.pi / 2.0`, the result may be very large or infinite. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.tanh {#math-tanh} ```flowscope math.tanh(value: float) -> float ``` Returns the hyperbolic tangent of a number. Returns a value from -1.0 to 1.0. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ### math.trunc {#math-trunc} ```flowscope math.trunc(value: float) -> float ``` Removes the fractional part of a number. Rounds toward zero: `1.8` becomes `1.0` and `-1.8` becomes `-1.0`. The result is a float. Check for null or use `??` to supply a fallback before calling. Null or NaN inputs return null. | Parameter | Type | |---|---| | `value` | `float` | **Returns** `float` ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `math.e` | `float` | `2.718281828459045` | Euler's number, the base of natural logarithms. | | `math.epsilon` | `float` | `2.220446049250313e-16` | The gap between 1.0 and the next larger float. | | `math.maxFloat` | `float` | `1.7976931348623157e+308` | The largest finite float. | | `math.maxInt` | `int` | `9223372036854775807` | The largest supported integer. | | `math.minFloat` | `float` | `-1.7976931348623157e+308` | The most negative finite float. | | `math.minInt` | `int` | `-9223372036854775807` | Lowest supported integer value (-9223372036854775807). | | `math.pi` | `float` | `3.141592653589793` | Pi, the ratio of a circle's circumference to its diameter. | | `math.tau` | `float` | `6.283185307179586` | Tau, equal to `2.0 * math.pi`. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/math --- # str reference > String helpers. Strings are immutable; every `str.*` function returns a new string. ## Functions ### str.concat {#str-concat} ```flowscope str.concat(values: scalar...) -> string ``` Joins values into one string. Pass values directly; they are converted to text using the same rules as `str.format` and joined in order. A null value appears as the text `null`. Text saved in state, collections, labels, tables, or logs counts toward the script's text limit while it is needed. Historical plot labels keep their text for the retained history. For one label showing the latest value, use a label pool with capacity one. A handler or state initializer can exceed the text limit even if its strings are temporary. Assigning a string to a variable does not reduce that limit. | Parameter | Type | |---|---| | `values` | `scalar` | **Returns** `string` ### str.contains {#str-contains} ```flowscope str.contains(text: string, needle: string) -> bool ``` Tests whether `text` contains `needle`. Looks for `needle` anywhere in `text`. Matching is case-sensitive. An empty `needle` always returns true. Check for null or use `??` to supply a fallback before calling with a nullable string. | Parameter | Type | |---|---| | `text` | `string` | | `needle` | `string` | **Returns** `bool` ### str.endsWith {#str-endswith} ```flowscope str.endsWith(text: string, suffix: string) -> bool ``` Tests whether `text` ends with `suffix`. Looks for `suffix` at the end of `text`. Matching is case-sensitive. An empty `suffix` always returns true. Check for null or use `??` to supply a fallback before calling with a nullable string. | Parameter | Type | |---|---| | `text` | `string` | | `suffix` | `string` | **Returns** `bool` ### str.format {#str-format} ```flowscope str.format(template: string, values: scalar...) -> string ``` Inserts values into a text template. `template` must be a string literal. `values` supplies the values for numbered placeholders, starting at `{0}`. A null value appears as the text `null`. For numbers, use `{n:[+][,][.digits][%]}`: - `+` always shows the sign. - `,` groups whole-number digits in threes. - `.digits` sets the decimal places. Ties round to the nearest even digit. - `%` multiplies by 100 before rounding and adds a percent sign. A plain `{n}` accepts any scalar value. A numeric format requires a number. Placeholders starting with a digit must have a valid index, format, and matching value, or the script is rejected before it runs. Other text in braces, including an unclosed `{`, stays unchanged. Named placeholders and `{{` escapes are not supported. Historical plot labels keep their text for the retained history. For one label showing the latest value, use a label pool with capacity one. A single handler can exceed the text limit even when its strings are temporary. | Parameter | Type | |---|---| | `template` | `string` | | `values` | `scalar` | **Returns** `string` ### str.lower {#str-lower} ```flowscope str.lower(text: string) -> string ``` Converts text to lowercase. Uses Unicode lowercase rules. Check for null or use `??` to supply a fallback before calling with a nullable string. | Parameter | Type | |---|---| | `text` | `string` | **Returns** `string` ### str.startsWith {#str-startswith} ```flowscope str.startsWith(text: string, prefix: string) -> bool ``` Tests whether `text` starts with `prefix`. Looks for `prefix` at the start of `text`. Matching is case-sensitive. An empty `prefix` always returns true. Check for null or use `??` to supply a fallback before calling with a nullable string. | Parameter | Type | |---|---| | `text` | `string` | | `prefix` | `string` | **Returns** `bool` ### str.trim {#str-trim} ```flowscope str.trim(text: string) -> string ``` Removes whitespace from both ends of text. Removes Unicode whitespace at either end. Whitespace inside the text stays unchanged. Check for null or use `??` to supply a fallback before calling with a nullable string. | Parameter | Type | |---|---| | `text` | `string` | **Returns** `string` ### str.upper {#str-upper} ```flowscope str.upper(text: string) -> string ``` Converts text to uppercase. Uses Unicode uppercase rules. Check for null or use `??` to supply a fallback before calling with a nullable string. | Parameter | Type | |---|---| | `text` | `string` | **Returns** `string` ## Properties ### str.isEmpty {#str-isempty} ```flowscope str.isEmpty: bool ``` Reports whether a string is empty. Returns true when the string has no characters. Whitespace still counts as text. Check for null or use `??` to supply a fallback before reading a nullable string. **Returns** `bool` ### str.len {#str-len} ```flowscope str.len: int ``` Counts the Unicode characters in a string. Counts Unicode scalar values, not bytes. A visible character made from several Unicode values, such as some emoji, can count as more than one. Check for null or use `??` to supply a fallback before reading a nullable string. **Returns** `int` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/str --- # color reference > Colours, gradients and colour scales. Colours are `#RRGGBB` or `#RRGGBBAA` literals or built with `color.rgb`. `color.up` and `color.down` follow the chart's candle colours; `color.gradient` and `color.scale` map values to colours. ## Functions ### color.a {#color-a} ```flowscope color.a(c: color) -> int ``` Returns the alpha channel from a color. `c` must be a non-null color. The result is the non-null alpha channel as an integer in the 0..=255 range. Six-digit hex colors have alpha 255; eight-digit hex colors carry their explicit alpha value. | Parameter | Type | |---|---| | `c` | `color` | **Returns** `int` ### color.b {#color-b} ```flowscope color.b(c: color) -> int ``` Returns the blue channel from a color. Returns the blue value of `c` as an integer from 0 to 255. Check for null or use `??` before passing a nullable color. | Parameter | Type | |---|---| | `c` | `color` | **Returns** `int` ### color.darken {#color-darken} ```flowscope color.darken(c: color, percent: float) -> color ``` Darkens a color by mixing it toward black. `c` is the starting color. `percent` sets how much to mix it toward black. The result is a non-null color with valid color-channel values. Check for null or use `??` before passing nullable inputs. | Parameter | Type | |---|---| | `c` | `color` | | `percent` | `float` | **Returns** `color` ### color.g {#color-g} ```flowscope color.g(c: color) -> int ``` Returns the green channel from a color. Returns the green value of `c` as an integer from 0 to 255. Check for null or use `??` before passing a nullable color. | Parameter | Type | |---|---| | `c` | `color` | **Returns** `int` ### color.gradient {#color-gradient} ```flowscope color.gradient(value: float, bottom: float, top: float, start: color, end: color) -> color ``` Blends between two colors based on a number. `value` chooses a position between `bottom` and `top`. Values are clamped to that range without an error. Returns `start` below the range or when `bottom == top`, and `end` above the range. Between the bounds, red, green, blue, and opacity change linearly. The result uses valid color-channel values. Check for null or use `??` before passing nullable numbers or colors. | Parameter | Type | |---|---| | `value` | `float` | | `bottom` | `float` | | `top` | `float` | | `start` | `color` | | `end` | `color` | **Returns** `color` ### color.lighten {#color-lighten} ```flowscope color.lighten(c: color, percent: float) -> color ``` Lightens a color by mixing it toward white. `c` is the starting color. `percent` sets how much to mix it toward white. The result is a non-null color with valid color-channel values. Check for null or use `??` before passing nullable inputs. | Parameter | Type | |---|---| | `c` | `color` | | `percent` | `float` | **Returns** `color` ### color.r {#color-r} ```flowscope color.r(c: color) -> int ``` Returns the red channel from a color. Returns the red value of `c` as an integer from 0 to 255. Check for null or use `??` before passing a nullable color. | Parameter | Type | |---|---| | `c` | `color` | **Returns** `int` ### color.rgb {#color-rgb} ```flowscope color.rgb(r: int, g: int, b: int, a: int = 255) -> color ``` Creates a color from red, green, blue, and opacity values. `r`, `g`, and `b` each use an integer from 0 to 255. Out-of-range values are clamped to the nearest end of that range. `a` sets opacity from 0 (transparent) to 255 (opaque) and defaults to 255. For example, `color.rgb(17, 34, 51)` equals `#112233`. All inputs must be non-null integers; fractions are not accepted. | Parameter | Type | |---|---| | `r` | `int` | | `g` | `int` | | `b` | `int` | | `a` | `int` | **Returns** `color` ### color.scale {#color-scale} ```flowscope color.scale(at: float[], colors: color[], space: ColorSpace = 2, easing: Easing = 1) -> ColorScale ``` Creates a color scale from a list of positions and colors. Define the scale during setup, then read colors with `.sample`. - `at` lists positions in ascending order. `colors` lists the matching colors. Both setup arrays need the same length and at least two entries. - `space` chooses `colorspace.rgb` or `colorspace.hsl`, defaulting to HSL. - `easing` chooses how colors change between positions: `easing.linear`, `easing.smoothstep`, or `easing.smootherstep`. The default is linear. Positions must be finite and cannot descend; invalid lists are rejected before the script runs. Equal adjacent positions create a sharp color change: the earlier color applies at that position, and the later color applies beyond it. Opacity always changes linearly. The scale cannot be saved in `state` or passed to other calls. Use its `.sample` method to read it. | Parameter | Type | |---|---| | `at` | `float[]` | | `colors` | `color[]` | | `space` | `ColorSpace` | | `easing` | `Easing` | **Returns** `ColorScale` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### color.withAlpha {#color-withalpha} ```flowscope color.withAlpha(c: color, a: int) -> color ``` Changes a color's opacity. `c` supplies the color. `a` sets opacity from 0 (transparent) to 255 (opaque). It is an integer, not a percentage. Out-of-range values are clamped to 0 or 255. Check for null or use `??` before passing a nullable color. | Parameter | Type | |---|---| | `c` | `color` | | `a` | `int` | **Returns** `color` ## Methods ### colorscale.sample {#colorscale-sample} ```flowscope colorscale.sample(t: float) -> color ``` Reads a color from a scale at a position. `t` chooses the position. Values outside the scale are clamped: below it returns the first color, above it returns the last. Null or NaN also returns the first color without an error. Between positions, the scale's easing and color space determine the blend. HSL takes the shortest path around the color wheel. A gray endpoint uses the other endpoint's hue. Opacity changes linearly in every color space. | Parameter | Type | |---|---| | `t` | `float` | **Returns** `color` **Available in** Handlers and functions called from handlers. ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `color.amber` | `color` | `#f59e0b` | Amber (#f59e0b). | | `color.black` | `color` | `#000000` | Black (#000000). | | `color.blue` | `color` | `#466be5` | Blue (#466be5). | | `color.brown` | `color` | `#a52a2a` | Brown (#a52a2a). | | `color.cyan` | `color` | `#00eeff` | Cyan (#00eeff). | | `color.down` | `color` | `#e81f59` | Bearish red (#e81f59). | | `color.emerald` | `color` | `#16c784` | Emerald (#16c784). | | `color.gray` | `color` | `#808080` | Gray (#808080). | | `color.green` | `color` | `#00ff00` | Green (#00ff00). | | `color.indigo` | `color` | `#2962ff` | Indigo (#2962ff). | | `color.lightskyblue` | `color` | `#87cefa` | Light sky blue (#87cefa). | | `color.magenta` | `color` | `#ff00ff` | Magenta (#ff00ff). | | `color.maroon` | `color` | `#800000` | Maroon (#800000). | | `color.navy` | `color` | `#000080` | Navy (#000080). | | `color.olive` | `color` | `#808000` | Olive (#808000). | | `color.orange` | `color` | `#ffa500` | Orange (#ffa500). | | `color.pink` | `color` | `#ffc0cb` | Pink (#ffc0cb). | | `color.purple` | `color` | `#800080` | Purple (#800080). | | `color.red` | `color` | `#e81f59` | Red (#e81f59). | | `color.silver` | `color` | `#9e9e9e` | Silver (#9e9e9e). | | `color.teal` | `color` | `#008080` | Teal (#008080). | | `color.transparent` | `color` | `#00000000` | Fully transparent black (#00000000). | | `color.up` | `color` | `#00ff00` | Bullish green (#00ff00). | | `color.white` | `color` | `#ffffff` | White (#ffffff). | | `color.yellow` | `color` | `#ffff00` | Yellow (#ffff00). | | `colorspace.hsl` | `ColorSpace` | `2` | HSL color-scale interpolation with shortest-path hue. | | `colorspace.rgb` | `ColorSpace` | `1` | RGB color-scale interpolation by channel. | | `easing.linear` | `Easing` | `1` | Linear segment easing, leaving the local position unchanged. | | `easing.smootherstep` | `Easing` | `3` | Smootherstep segment easing with t*t*t*(t*(6t-15)+10). | | `easing.smoothstep` | `Easing` | `2` | Smoothstep segment easing with t*t*(3-2t). | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/color --- # time reference > Calendar, session, timeframe and duration helpers. Times are UTC instants; bar times are period open times. Calendar functions take an optional IANA time zone. ## Functions ### tf {#tf} ```flowscope tf(value: string) -> timeframe ``` Converts a string to a timeframe. `value` uses case-sensitive units: `s`, `m`, `h`, `D`, `W`, `M`, or `Y`, for example `"15m"` or `"4h"`. The length must be positive. An empty, invalid, or out-of-range value stops the script with an error. String literals are checked before the script runs. The result is never null. | Parameter | Type | |---|---| | `value` | `string` | **Returns** `timeframe` ### time.bars {#time-bars} ```flowscope time.bars(n: int, tf: timeframe? = null) -> duration ``` Returns the nominal duration of `n` bars on `tf`. `n` is the bar count. `tf` is optional; null or omitted `tf` uses the current timeframe: the chart's on a chart, the window's in a window, otherwise the run configuration's. Fixed units use exact spans, while `M` and `Y` use nominal spans of 30 and 365 days. Use `time.shiftBars` when period boundaries matter. | Parameter | Type | |---|---| | `n` | `int` | | `tf` | `timeframe?` | **Returns** `duration` ### time.ceil {#time-ceil} ```flowscope time.ceil(ts: time, tf: timeframe) -> time ``` Rounds `ts` up to the start of the next `tf` period unless already aligned. `ts` is returned unchanged when already aligned to `tf`; otherwise the next `tf` boundary is returned after flooring. Arguments are non-null. | Parameter | Type | |---|---| | `ts` | `time` | | `tf` | `timeframe` | **Returns** `time` ### time.clock {#time-clock} ```flowscope time.clock(ts: time) -> string ``` Formats the UTC clock portion of `ts`. `ts` is formatted as a UTC clock string with the format `{HH}:{mm}:{ss}`. The result matches `time.format` with that template and is never null. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `string` ### time.count {#time-count} ```flowscope time.count(from: time, to: time, tf: timeframe) -> int ``` Counts `tf` boundaries from `from` to `to`. `from` and `to` are mapped to their containing `tf` periods, then the signed boundary difference is returned. `W`, `M`, and `Y` use UTC calendar periods. Reversing `from` and `to` negates the count. | Parameter | Type | |---|---| | `from` | `time` | | `to` | `time` | | `tf` | `timeframe` | **Returns** `int` ### time.date {#time-date} ```flowscope time.date(ts: time) -> string ``` Formats the UTC date portion of `ts`. `ts` is formatted as a UTC date string with the format `{YYYY}-{MM}-{DD}`. The result matches `time.format` with that template and is never null. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `string` ### time.dayOfMonth {#time-dayofmonth} ```flowscope time.dayOfMonth(ts: time) -> int ``` Returns the UTC day of month of `ts`. `ts` is read in the UTC calendar month. The result is 1 through 31 and is always a non-null int. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.dayOfWeek {#time-dayofweek} ```flowscope time.dayOfWeek(ts: time) -> int ``` Returns the UTC ISO day of week of `ts`. `ts` is read in the UTC calendar. The result is 1 for Monday through 7 for Sunday and is always a non-null int. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.dayStart {#time-daystart} ```flowscope time.dayStart(ts: time) -> time ``` Returns the UTC day start containing `ts`. `ts` is rounded to 00:00:00 UTC on its calendar day. This matches the fixed one-day boundary for UTC timestamps. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `time` ### time.days {#time-days} ```flowscope time.days(n: int) -> duration ``` Constructs a duration of `n` fixed 24-hour days. `n` is multiplied by 86,400 seconds. Calendar dates and daylight-saving rules do not affect this fixed-duration constructor. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `duration` ### time.diff {#time-diff} ```flowscope time.diff(a: time, b: time) -> duration ``` Returns the difference between two timestamps. Returns `a - b` as a signed duration in seconds. Both timestamps must be non-null. Check for null before calling. | Parameter | Type | |---|---| | `a` | `time` | | `b` | `time` | **Returns** `duration` ### time.durationSeconds {#time-durationseconds} ```flowscope time.durationSeconds(d: duration) -> int ``` Returns the raw second count of a duration. `d` is returned as its signed second count. Use `??` or a null guard before the call when the duration expression is nullable. | Parameter | Type | |---|---| | `d` | `duration` | **Returns** `int` ### time.floor {#time-floor} ```flowscope time.floor(ts: time, tf: timeframe) -> time ``` Rounds `ts` down to the start of its `tf` period. Fixed units are aligned to exact spans from the Unix epoch. `W` is aligned to Monday 00:00 UTC; `M` to the first day of its global month group; and `Y` to January 1 of its global year group. Multi-period groups remain continuous across year boundaries. | Parameter | Type | |---|---| | `ts` | `time` | | `tf` | `timeframe` | **Returns** `time` ### time.format {#time-format} ```flowscope time.format(ts: time, template: string) -> string ``` Formats a timestamp as UTC text. `ts` is the timestamp. `template` must be a string literal. Use tokens in braces, for example `"{YYYY}-{MM}-{DD} {HH}:{mm}"`. Supported tokens: `YYYY`, `YY`, `M`, `MM`, `D`, `DD`, `H`, `HH`, `m`, `mm`, `s`, `ss`, `dayOfWeek`, and `unix`. Invalid templates and unknown tokens are rejected before the script runs. | Parameter | Type | |---|---| | `ts` | `time` | | `template` | `string` | **Returns** `string` ### time.fromUnixSeconds {#time-fromunixseconds} ```flowscope time.fromUnixSeconds(seconds: int) -> time ``` Creates a timestamp from Unix seconds. `seconds` counts from 1970-01-01 00:00:00 UTC. Negative values represent earlier times. | Parameter | Type | |---|---| | `seconds` | `int` | **Returns** `time` ### time.hour {#time-hour} ```flowscope time.hour(ts: time) -> int ``` Returns the UTC hour of day of `ts`. `ts` is read as a UTC time of day. The result is 0 through 23 and is always a non-null int. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.hours {#time-hours} ```flowscope time.hours(n: int) -> duration ``` Creates a duration of `n` hours. Each hour is 3,600 seconds. `n` can be negative. Arithmetic with a null duration returns null. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `duration` ### time.isAligned {#time-isaligned} ```flowscope time.isAligned(ts: time, tf: timeframe) -> bool ``` Checks whether `ts` is exactly on a `tf` boundary. `ts` is aligned when it equals `time.floor(ts, tf)`, including UTC calendar alignment for `W`, `M`, and `Y`. The result is always a non-null bool. | Parameter | Type | |---|---| | `ts` | `time` | | `tf` | `timeframe` | **Returns** `bool` ### time.isCalendar {#time-iscalendar} ```flowscope time.isCalendar(tf: timeframe) -> bool ``` Checks whether a timeframe uses a calendar-length unit. `tf` returns true for UTC calendar-aligned `W`, `M`, and `Y` timeframes. Fixed `s`, `m`, `h`, and `D` timeframes return false. | Parameter | Type | |---|---| | `tf` | `timeframe` | **Returns** `bool` ### time.isNewPeriod {#time-isnewperiod} ```flowscope time.isNewPeriod(ts: time, prev: time?, tf: timeframe) -> bool ``` Checks whether `ts` starts a different `tf` period than `prev`. `ts` is compared with `prev` after both are floored to `tf`. If `prev` is null, the result is true so first samples can initialize period state without a separate guard. The return is always bool. | Parameter | Type | |---|---| | `ts` | `time` | | `prev` | `time?` | | `tf` | `timeframe` | **Returns** `bool` ### time.minute {#time-minute} ```flowscope time.minute(ts: time) -> int ``` Returns the UTC minute of hour of `ts`. `ts` is read as a UTC time of day. The result is 0 through 59 and is always a non-null int. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.minutes {#time-minutes} ```flowscope time.minutes(n: int) -> duration ``` Creates a duration of `n` minutes. Each minute is 60 seconds. `n` can be negative. Values too large for a duration are rejected, as with duration arithmetic. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `duration` ### time.month {#time-month} ```flowscope time.month(ts: time) -> int ``` Returns the UTC month number of `ts`. `ts` is read in the UTC calendar. The result is 1 through 12 and is always a non-null int. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.monthStart {#time-monthstart} ```flowscope time.monthStart(ts: time) -> time ``` Returns the UTC month start containing `ts`. `ts` is rounded to 00:00:00 UTC on day 1 of its calendar month. The return is a non-null timestamp. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `time` ### time.next {#time-next} ```flowscope time.next(ts: time, tf: timeframe) -> time ``` Returns the next `tf` boundary after `ts`. `ts` is floored to its `tf` period, then one boundary is added. `W`, `M`, and `Y` use their UTC calendar alignment rather than nominal durations. | Parameter | Type | |---|---| | `ts` | `time` | | `tf` | `timeframe` | **Returns** `time` ### time.nominalDuration {#time-nominalduration} ```flowscope time.nominalDuration(tf: timeframe) -> duration ``` Returns the nominal duration of one timeframe period. `tf` returns its nominal duration. Fixed units use exact spans; calendar `M` and `Y` use 30-day and 365-day spans when a duration value is required. | Parameter | Type | |---|---| | `tf` | `timeframe` | **Returns** `duration` ### time.prev {#time-prev} ```flowscope time.prev(ts: time, tf: timeframe) -> time ``` Returns the previous `tf` boundary before `ts`. `ts` is floored to its `tf` period, then one boundary is subtracted. `W`, `M`, and `Y` use their UTC calendar alignment rather than nominal durations. | Parameter | Type | |---|---| | `ts` | `time` | | `tf` | `timeframe` | **Returns** `time` ### time.second {#time-second} ```flowscope time.second(ts: time) -> int ``` Returns the UTC second of minute of `ts`. Reads `ts` in UTC and returns 0 to 59. Leap seconds are not represented. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.seconds {#time-seconds} ```flowscope time.seconds(n: int) -> duration ``` Creates a duration of `n` seconds. `n` is the number of seconds and can be negative. Arithmetic with a null duration returns null. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `duration` ### time.session {#time-session} ```flowscope time.session(start: string, end: string, name: string? = null) -> session ``` Defines a daily time window in UTC. `start` is included and `end` is excluded. If `end <= start`, the session wraps across midnight. `name` is optional and defaults to null. Pass clock times as string literals. Invalid times are rejected before the script runs. | Parameter | Type | |---|---| | `start` | `string` | | `end` | `string` | | `name` | `string?` | **Returns** `session` ### time.sessionMinutes {#time-sessionminutes} ```flowscope time.sessionMinutes(start: int, end: int) -> session ``` Defines a daily UTC window using minutes after midnight. `start` and `end` each accept 0 to 1440, inclusive. 1440 means the end of the day, like `24:00`. The start is included and the end is excluded. If `end <= start`, the session wraps across midnight. The session has no name. For example, `time.sessionMinutes(570, 960)` equals `time.session("09:30", "16:00")` and supports the same session methods. Arguments can change while the script runs. An out-of-range value stops the script with an error; a constant out-of-range value is rejected before the script runs. To convert decimal hours, use `int(math.round(hours * 60.0, 0)) ?? 0`; `int(x)` can return null. | Parameter | Type | |---|---| | `start` | `int` | | `end` | `int` | **Returns** `session` ### time.shiftBars {#time-shiftbars} ```flowscope time.shiftBars(ts: time, n: int, tf: timeframe? = null) -> time ``` Moves `ts` by `n` timeframe boundaries. `ts` is the timestamp to move, `n` is the signed boundary count, and `tf` is optional. Null or omitted `tf` uses the current timeframe: the chart's on a chart, the window's in a window, otherwise the run configuration's. Nonzero `n` snaps to the current boundary before stepping; `n == 0` returns `ts`. Calendar-aligned `W`, `M`, and `Y` timeframes use UTC boundaries. | Parameter | Type | |---|---| | `ts` | `time` | | `n` | `int` | | `tf` | `timeframe?` | **Returns** `time` ### time.tfFromSeconds {#time-tffromseconds} ```flowscope time.tfFromSeconds(seconds: int) -> timeframe ``` Creates a fixed-length timeframe from seconds. `seconds` must be positive and fit within the timeframe limit. Other values stop the script with an error. The result uses seconds, minutes, hours, or days: 60 becomes `1m`, 900 becomes `15m`, 3600 becomes `1h`, and 86400 becomes `1D`. A value of 90 stays `90s`. A value of 604800 becomes `7D`; calendar weeks, months, and years are not produced. You can use this during setup. For example, `time.tfFromSeconds(time.tfSeconds(context.timeframe) * 4)` selects four times the chart timeframe for a subscription. | Parameter | Type | |---|---| | `seconds` | `int` | **Returns** `timeframe` ### time.tfSeconds {#time-tfseconds} ```flowscope time.tfSeconds(tf: timeframe) -> int ``` Returns a timeframe's length in seconds. For `tf`, `1m` returns 60, `4h` returns 14400, `1D` returns 86400, and `1W` returns 604800. Months and years use estimates of 30 and 365 days. Use `time.isCalendar` before treating the result as an exact bar length. This equals `time.durationSeconds(time.nominalDuration(tf))`. You can use it with constants or `context.timeframe` during setup, including in `subscribe` arguments. | Parameter | Type | |---|---| | `tf` | `timeframe` | **Returns** `int` ### time.unixSeconds {#time-unixseconds} ```flowscope time.unixSeconds(ts: time?) -> int? ``` Converts a timestamp to Unix seconds. `ts` is measured in seconds since 1970-01-01 00:00:00 UTC. Returns that count as an int, or null if `ts` is null. | Parameter | Type | |---|---| | `ts` | `time?` | **Returns** `int?` ### time.weekStart {#time-weekstart} ```flowscope time.weekStart(ts: time) -> time ``` Returns the start of the UTC week containing `ts`. Rounds `ts` down to Monday at 00:00:00 UTC. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `time` ### time.weeks {#time-weeks} ```flowscope time.weeks(n: int) -> duration ``` Constructs a duration of `n` fixed seven-day weeks. `n` is multiplied by 604,800 seconds, so one week of duration equals seven fixed days. A `1W` timeframe is different: timeframe weeks align to calendar weeks. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `duration` ### time.year {#time-year} ```flowscope time.year(ts: time) -> int ``` Returns the UTC calendar year of `ts`. Reads `ts` in UTC using Gregorian calendar rules, including for dates before the calendar was introduced. Returns a non-null integer year. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `int` ### time.yearStart {#time-yearstart} ```flowscope time.yearStart(ts: time) -> time ``` Returns the UTC year start containing `ts`. `ts` is rounded to 00:00:00 UTC on January 1 of its calendar year. The return is a non-null timestamp. | Parameter | Type | |---|---| | `ts` | `time` | **Returns** `time` ## Methods ### time.contains {#time-contains} ```flowscope time.contains(time: time) -> bool ``` Reports whether a timestamp is inside the session. Checks `time` against the session's UTC window. The start is included and the end is excluded. Check for null before calling on a nullable session. | Parameter | Type | |---|---| | `time` | `time` | **Returns** `bool` ### time.entered {#time-entered} ```flowscope time.entered(prev: time, time: time) -> bool ``` Tests whether the current timestamp entered the session. `prev` is the earlier timestamp and `time` is the current timestamp. The result is true when `prev` was outside the session and `time` is inside it. Both timestamps are non-null UTC instants. | Parameter | Type | |---|---| | `prev` | `time` | | `time` | `time` | **Returns** `bool` ### time.exited {#time-exited} ```flowscope time.exited(prev: time, time: time) -> bool ``` Tests whether the current timestamp exited the session. `prev` is the earlier timestamp and `time` is the current timestamp. The result is true when `prev` was inside the session and `time` is outside it. Both timestamps are non-null UTC instants. | Parameter | Type | |---|---| | `prev` | `time` | | `time` | `time` | **Returns** `bool` ### time.overlaps {#time-overlaps} ```flowscope time.overlaps(time: time, end: time) -> bool ``` Reports whether a time range overlaps the session. `time` is the start of the UTC range, included in the range. `end` is its end, excluded from the range. Both must be non-null; check for null before calling. | Parameter | Type | |---|---| | `time` | `time` | | `end` | `time` | **Returns** `bool` ### time.windowEnd {#time-windowend} ```flowscope time.windowEnd(time: time) -> time? ``` Returns the session window end containing a timestamp. `time` selects the session window. Returns the UTC end timestamp for that window, or null when `time` is outside the session. The input timestamp is non-null. | Parameter | Type | |---|---| | `time` | `time` | **Returns** `time?` ### time.windowStart {#time-windowstart} ```flowscope time.windowStart(time: time) -> time? ``` Returns the session window start containing a timestamp. `time` selects the session window. Returns the UTC start timestamp for that window, or null when `time` is outside the session. The input timestamp is non-null. | Parameter | Type | |---|---| | `time` | `time` | **Returns** `time?` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/time --- # collections reference > Arrays, rolling windows and maps with their methods. Arrays `T[]`, rolling windows `rolling` and maps `map`. Keep a collection across bars in `state`. Reads return `T?`. `map`, `filter` and `reduce` take lambdas. ## Methods ### collection.absSum {#collection-abssum} ```flowscope collection.absSum() -> same_item_numeric ``` Adds the absolute values of the non-null numbers. Available for numeric collections. Null float elements are skipped. The result keeps the element type. Returns 0 if the collection is empty or every element is null. **Returns** `same_item_numeric` ### collection.avg {#collection-avg} ```flowscope collection.avg() -> float? ``` Returns the average of the non-null numbers. Available for numeric collections. Null float elements are skipped. Divides by the number of non-null elements. The result is always a float. Returns null if the collection is empty or every element is null. **Returns** `float?` ### collection.avgLast {#collection-avglast} ```flowscope collection.avgLast(n: int) -> float? ``` Averages the numbers in the last `n` elements. `n` chooses how many elements to inspect from the newest end. Use a value from 0 to `len`, inclusive; other values stop the script with an error. Available for numeric collections. Null float elements are skipped. Divides by the number of non-null elements in the selection and returns a float. An empty or all-null selection, including `n = 0`, returns null. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `float?` ### collection.clear {#collection-clear} ```flowscope collection.clear() -> void ``` Removes every item from the collection. After clearing, `len` is zero. Arrays and rolling buffers have no elements, and maps have no keys. Stored null values are removed too. Frozen maps cannot be cleared; attempting this is rejected before the script runs. ### collection.countEq {#collection-counteq} ```flowscope collection.countEq(value: Item) -> int ``` Counts the elements equal to a value. Returns the number of elements equal to `value`. +0.0 and -0.0 compare equal. The count is never null. Null elements never match a search value, even null. For struct elements, all fields are compared. Two null fields count as equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int` ### collection.countGt {#collection-countgt} ```flowscope collection.countGt(value: Item) -> int ``` Counts the numbers greater than a value. Compares each numeric element with `value`. Null elements are skipped, and a null `value` matches nothing. Infinities compare normally. +0.0 and -0.0 compare equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int` ### collection.countGte {#collection-countgte} ```flowscope collection.countGte(value: Item) -> int ``` Counts the numbers greater than or equal to a value. Compares each numeric element with `value`. Null elements are skipped, and a null `value` matches nothing. Infinities compare normally. +0.0 and -0.0 compare equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int` ### collection.countLt {#collection-countlt} ```flowscope collection.countLt(value: Item) -> int ``` Counts the numbers less than a value. Compares each numeric element with `value`. Null elements are skipped, and a null `value` matches nothing. Infinities compare normally. +0.0 and -0.0 compare equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int` ### collection.countLte {#collection-countlte} ```flowscope collection.countLte(value: Item) -> int ``` Counts the numbers less than or equal to a value. Compares each numeric element with `value`. Null elements are skipped, and a null `value` matches nothing. Infinities compare normally. +0.0 and -0.0 compare equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int` ### collection.filter {#collection-filter} ```flowscope collection.filter(p: |x| bool) -> Item[] ``` Creates a new array containing the elements that pass a test. `p` receives each stored element from oldest to newest, including null floats. Return true to keep an element or false to skip it; the result cannot be null. The new array keeps the element type, order, and capacity of the source. A rolling buffer also returns an ordinary array and visits only its current elements. | Parameter | Type | |---|---| | `p` | `Item` | **Returns** `Item[]` ### collection.first {#collection-first} ```flowscope collection.first() -> Item? ``` Reads the oldest element. Returns null if the collection is empty or the stored float value is null. Use `last` to read the newest element. **Returns** `Item?` ### collection.get {#collection-get} ```flowscope collection.get(index: int) -> Item? ``` Reads the element at an index. `index` counts from 0, starting with the oldest element. Returns null for a negative index or an index at or beyond `len`, including rolling-buffer slots that have not filled yet. A stored float can also be null. Other element types cannot store null. | Parameter | Type | |---|---| | `index` | `int` | **Returns** `Item?` ### collection.includes {#collection-includes} ```flowscope collection.includes(value: Item) -> bool ``` Reports whether the collection contains a value. Returns true if an element equals `value`, otherwise false. +0.0 and -0.0 compare equal; infinities compare normally. Null elements never match a search value, even null. For struct elements, all fields are compared. Two null fields count as equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `bool` ### collection.indexOf {#collection-indexof} ```flowscope collection.indexOf(value: Item) -> int? ``` Finds the first element equal to a value. Returns the first matching index or null if there is no match. Index 0 is the oldest element. Null elements never match a search value, even null. For struct elements, all fields are compared. Two null fields count as equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int?` ### collection.last {#collection-last} ```flowscope collection.last() -> Item? ``` Reads the newest element. Returns null if the collection is empty or the stored float value is null. Use `first` to read the oldest element. **Returns** `Item?` ### collection.lastIndexOf {#collection-lastindexof} ```flowscope collection.lastIndexOf(value: Item) -> int? ``` Finds the last element equal to a value. Returns the last matching index or null if there is no match. Index 0 is still the oldest element; the search starts from the newest end. Null elements never match a search value, even null. For struct elements, all fields are compared. Two null fields count as equal. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `int?` ### collection.map {#collection-map} ```flowscope collection.map(f: |x| U) -> U[] ``` Creates a new array by transforming each element. `f` receives each stored element from oldest to newest, including null floats. Its return value becomes the corresponding element in the new array. The new array has the same length and capacity as the source. A rolling buffer also returns an ordinary array and visits only the elements it currently holds. | Parameter | Type | |---|---| | `f` | `Item` | **Returns** `U[]` ### collection.map.delete {#collection-map-delete} ```flowscope collection.map.delete(key: Key) -> bool ``` Removes a key and its value. `key` selects the entry to remove. Returns true if the key existed or false if it was missing, including when the stored value was null. Frozen maps cannot be changed. A null `key` is rejected before the script runs when it is known then; otherwise it stops the script with an error. | Parameter | Type | |---|---| | `key` | `Key` | **Returns** `bool` ### collection.map.get {#collection-map-get} ```flowscope collection.map.get(key: Key) -> Item? ``` Reads the value stored for a key. `key` selects the entry. A missing key returns null; use `??` for a fallback. Stored null values also return null. On a frozen map, a constant key known to exist can return a non-null type. A null `key` is rejected before the script runs when it is known then; otherwise it stops the script with an error. | Parameter | Type | |---|---| | `key` | `Key` | **Returns** `Item?` ### collection.map.has {#collection-map-has} ```flowscope collection.map.has(key: Key) -> bool ``` Reports whether a key exists in the map. `key` selects the entry to check. Returns true even if its stored value is null. A null `key` is rejected before the script runs when it is known then; otherwise it stops the script with an error. | Parameter | Type | |---|---| | `key` | `Key` | **Returns** `bool` ### collection.map.set {#collection-map-set} ```flowscope collection.map.set(key: Key, value: Item) -> void ``` Adds a key and value or replaces an existing value. Stores `value` at `key`. Replacing a value keeps the key's position in insertion order. Values may be null. Adding a new key when full stops the script with an error. Replacing an existing value when full is allowed. Frozen maps cannot be changed. A null `key` is rejected before the script runs when it is known then; otherwise it stops the script with an error. | Parameter | Type | |---|---| | `key` | `Key` | | `value` | `Item` | ### collection.max {#collection-max} ```flowscope collection.max() -> same_item_numeric? ``` Returns the largest non-null number. Available for numeric collections. Null float elements are skipped. Infinities compare normally. A non-null result keeps the element type. Returns null if the collection is empty or every element is null. **Returns** `same_item_numeric?` ### collection.min {#collection-min} ```flowscope collection.min() -> same_item_numeric? ``` Returns the smallest non-null number. Available for numeric collections. Null float elements are skipped. Infinities compare normally. A non-null result keeps the element type. Returns null if the collection is empty or every element is null. **Returns** `same_item_numeric?` ### collection.pop {#collection-pop} ```flowscope collection.pop() -> Item? ``` Removes and returns the newest element. Returns null if the collection is empty. A removed float element can also be null. Rolling buffers support `pop` for every element type. **Returns** `Item?` ### collection.push {#collection-push} ```flowscope collection.push(value: Item) -> per-receiver ``` Adds an element to the end of the collection. `value` becomes the newest element. - Arrays stop the script with an error if they are already full. - Rolling buffers remove and return the oldest element when full. If there was room, they return null. Float collections can store null. Null values for other element types are rejected before the script runs. | Parameter | Type | |---|---| | `value` | `Item` | **Returns** `per-receiver` ### collection.range {#collection-range} ```flowscope collection.range() -> same_item_numeric? ``` Returns the difference between the largest and smallest non-null numbers. Available for numeric collections. Null float elements are skipped. A non-null result keeps the element type. Returns null if the collection is empty or every element is null. **Returns** `same_item_numeric?` ### collection.reduce {#collection-reduce} ```flowscope collection.reduce(init: R, f: |acc, x| R) -> R ``` Combines the elements into one value. `init` is the starting total. `f` receives the current total and the next element, then returns the new total. Elements are visited from oldest to newest, including null floats. An empty collection returns `init` unchanged. A rolling buffer visits only the elements it currently holds. | Parameter | Type | |---|---| | `init` | `scalar` | | `f` | `Item` | **Returns** `R` ### collection.set {#collection-set} ```flowscope collection.set(index: int, value: Item) -> void ``` Replaces the element at an index. `index` counts from 0, starting with the oldest element. `value` replaces that element; `a[i] = v` does the same thing. A negative index or an index at or beyond `len` stops the script with an error. Float collections can store null. Null values for other element types are rejected before the script runs. | Parameter | Type | |---|---| | `index` | `int` | | `value` | `Item` | ### collection.shift {#collection-shift} ```flowscope collection.shift() -> Item? ``` Removes and returns the oldest element. Returns null if the collection is empty. A removed float element can also be null. Rolling buffers support `shift` for every element type. **Returns** `Item?` ### collection.stdev {#collection-stdev} ```flowscope collection.stdev() -> float? ``` Returns the population standard deviation of the non-null numbers. Available for numeric collections. Null float elements are skipped. This is the square root of the population variance. Returns null if the collection is empty or every element is null. **Returns** `float?` ### collection.sum {#collection-sum} ```flowscope collection.sum() -> same_item_numeric ``` Adds all non-null numbers. Available for numeric collections. Null float elements are skipped. The result keeps the element type. Returns 0 if the collection is empty or every element is null. **Returns** `same_item_numeric` ### collection.sumLast {#collection-sumlast} ```flowscope collection.sumLast(n: int) -> same_item_numeric ``` Adds the numbers in the last `n` elements. `n` chooses how many elements to inspect from the newest end. Use a value from 0 to `len`, inclusive; other values stop the script with an error. Available for numeric collections. Null float elements are skipped. An empty selection, including `n = 0`, returns 0. | Parameter | Type | |---|---| | `n` | `int` | **Returns** `same_item_numeric` ### collection.variance {#collection-variance} ```flowscope collection.variance() -> float? ``` Returns the population variance of the non-null numbers. Available for numeric collections. Null float elements are skipped. Divides by the number of non-null elements. Returns null if the collection is empty or every element is null. **Returns** `float?` ## Properties ### collection.capacity {#collection-capacity} ```flowscope collection.capacity: int ``` Returns the maximum number of items the collection can hold. Arrays, rolling buffers, and mutable maps use the capacity set when they were created. A frozen map's capacity equals its length. **Returns** `int` ### collection.isEmpty {#collection-isempty} ```flowscope collection.isEmpty: bool ``` Reports whether the collection is empty. Returns true when `len == 0`. A map with a key whose value is null is still non-empty. **Returns** `bool` ### collection.isFull {#collection-isfull} ```flowscope collection.isFull: bool ``` Reports whether a rolling buffer currently holds its full capacity. `isFull` is true once a rolling buffer's `len` equals its `capacity`. Plain arrays do not support `isFull`; compare `len` with `capacity` when an array needs the same test. **Returns** `bool` ### collection.len {#collection-len} ```flowscope collection.len: int ``` Returns the number of items in the collection. Arrays and rolling buffers count stored elements, including null float values. Maps count keys, including keys whose values are null. **Returns** `int` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/collections --- # plot reference > Series plots, fills and their styles. Declare a plot at top level (`plot fast = plot.line(...)`) and feed it from handlers with `fast.plot(value)`. A null value leaves a gap. `fill.between` shades between two plots. ## Functions ### fill.between {#fill-between} ```flowscope fill.between(a: PlotLine, b: PlotLine, color: color? = null) -> Fill ``` Creates a fill between two line plots. `a` and `b` must be line plots on the same pane, both using subscribed bars. Other pairs are rejected before the script runs. `color` sets the fill color; null uses the theme default. Call `fill` on each update to draw the fill, optionally passing a different color. Updating either line without calling `fill` leaves that span unfilled. | Parameter | Type | |---|---| | `a` | `PlotLine` | | `b` | `PlotLine` | | `color` | `color?` | **Returns** `Fill` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.arrow {#plot-arrow} ```flowscope plot.arrow(title: string = "", colorUp: color? = null, colorDown: color? = null, minHeight: float? = null, maxHeight: float? = null, paddingPx: float? = null, shaftWidth: float? = null, headLengthPx: float? = null, headWidthRatio: float? = null, offset: int = 0, showLabel: bool = true, showValue: bool = true, on: Surface? = null) -> PlotArrow ``` Creates arrows whose direction and height follow a value. Positive and negative values choose the direction. A larger absolute value makes a taller arrow. Null skips a bar. - `title` names the plot. - `colorUp` and `colorDown` set the direction colors. - `minHeight` and `maxHeight` set the height range, each from 0.0 to 200.0 pixels. - `paddingPx` and `headLengthPx` each accept 0.0 to 50.0 pixels. - `shaftWidth` accepts 0.5 to 20.0 pixels. - `headWidthRatio` accepts 1.0 to 5.0. Null geometry options use the theme default. - `offset` shifts values along the bars, from -500 to 500. Other values are rejected before the script runs. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Null uses the main price pane. | Parameter | Type | |---|---| | `title` | `string` | | `colorUp` | `color?` | | `colorDown` | `color?` | | `minHeight` | `float?` | | `maxHeight` | `float?` | | `paddingPx` | `float?` | | `shaftWidth` | `float?` | | `headLengthPx` | `float?` | | `headWidthRatio` | `float?` | | `offset` | `int` | | `showLabel` | `bool` | | `showValue` | `bool` | | `on` | `Surface?` | **Returns** `PlotArrow` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.bar {#plot-bar} ```flowscope plot.bar(title: string = "", color: color? = null, offset: int = 0, showLabel: bool = true, showValue: bool = true, capacity: int = 512, on: Surface? = null) -> PlotBar ``` Creates an OHLC bar plot. - `title` names the plot. - `color` sets the bar color. Null uses the theme default. - `offset` shifts values along the bars, from -500 to 500. Other values are rejected before the script runs. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Null uses the main price pane. - `capacity` accepts 1 to 4096, but does not affect this plot: OHLC bars only support subscribed bars. Use `plot` to draw each bar. Placement on a panel with an explicit keyed axis is rejected before the script runs. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `offset` | `int` | | `showLabel` | `bool` | | `showValue` | `bool` | | `capacity` | `int` | | `on` | `Surface?` | **Returns** `PlotBar` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.bg {#plot-bg} ```flowscope plot.bg(title: string = "", color: color? = null, on: Surface? = null) -> PlotBg ``` Creates background highlights behind the chart. `title` names the plot. `color` sets the tint; null uses the theme default. `on` chooses the pane, with null using the main price pane. Call `plot` to tint a bar across the full pane height, behind other drawings. Skip the call to leave the bar clear. Background plots only support subscribed bars. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `on` | `Surface?` | **Returns** `PlotBg` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.candle {#plot-candle} ```flowscope plot.candle(title: string = "", bodyColor: color? = null, wickColor: color? = null, borderColor: color? = null, offset: int = 0, showLabel: bool = true, showValue: bool = true, capacity: int = 512, on: Surface? = null) -> PlotCandle ``` Creates a candlestick plot. - `title` names the plot. - `bodyColor`, `wickColor`, and `borderColor` set the candle colors. Null uses the theme default. - `offset` shifts values along the bars, from -500 to 500. Other values are rejected before the script runs. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Null uses the main price pane. Use `plot` for candles along subscribed bars. On a time-axis panel, use `set` with your own candle times. `capacity` limits keys to 512 by default, with a maximum of 4096. Adding a new key when full stops the script with an error. | Parameter | Type | |---|---| | `title` | `string` | | `bodyColor` | `color?` | | `wickColor` | `color?` | | `borderColor` | `color?` | | `offset` | `int` | | `showLabel` | `bool` | | `showValue` | `bool` | | `capacity` | `int` | | `on` | `Surface?` | **Returns** `PlotCandle` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.cells {#plot-cells} ```flowscope plot.cells(title: string = "", columns: int = 1, scale: CellScale = cellscale.bar, color: color? = null, borderColor: color? = null, textColor: color? = null, candles: CellCandles = cellcandles.overlay, format: CellFormat = cellformat.compact, decimals: int? = null, sign: bool = false, grouping: bool = false, on: Surface? = null) -> PlotCells ``` Creates a plot of rows of cells inside each bar, such as a footprint or a per-bar profile. A cell is a rectangle over a price range inside a bar, with an optional width, colours and a number. - `columns` splits each bar into 1 to 4 columns side by side. Other values are rejected before the script runs. - `scale` says what a full-width cell means: `cellscale.bar` the largest weight on its own bar, `cellscale.view` the largest on the visible bars, `cellscale.unit` the weight is the width, from 0 to 1. - `color`, `borderColor` and `textColor` are the defaults of cells that pass none. Without a `color` or `borderColor` there is no fill or no border; without a `textColor` the chart's text colour is used. - `candles` says how the chart's own candles are drawn while the plot shows on the price pane: `cellcandles.overlay` as they are, `cellcandles.side` as a thin strip at the left of each bar, `cellcandles.hidden` not at all. With several cells plots, hidden wins over side, side over overlay. - `format`, `decimals` (0 to 8), `sign` and `grouping` say how a cell's `value` is written: `3.21K` (compact), `3214.56` (fixed), `15.3%` (percent, of a ratio), `+3,215` with a sign and grouping. A number that does not fit its cell is written shorter, and left out when nothing fits. - `on` chooses the pane. Null is the price pane. Add cells with `cell` and empty a bar with `clear`. A cells plot has no value in the legend and does not change the price scale. **Limits** A bar holds at most 1024 cells per plot. A script keeps 400 000 cells over all its cells plots; beyond that the oldest bars lose theirs first. | Parameter | Type | |---|---| | `title` | `string` | | `columns` | `int` | | `scale` | `CellScale` | | `color` | `color?` | | `borderColor` | `color?` | | `textColor` | `color?` | | `candles` | `CellCandles` | | `format` | `CellFormat` | | `decimals` | `int?` | | `sign` | `bool` | | `grouping` | `bool` | | `on` | `Surface?` | **Returns** `PlotCells` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.histogram {#plot-histogram} ```flowscope plot.histogram(title: string = "", color: color? = null, horizontal: bool = false, showLabel: bool = true, showValue: bool = true, capacity: int = 512, on: Surface? = null) -> PlotHistogram ``` Creates a histogram or bar chart. - `title` names the plot. - `color` sets the bar color. Null uses the theme default. - `horizontal` draws sideways bars. It requires an explicit category-axis panel; other placements are rejected before the script runs. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Without it, a vertical histogram uses the script's shared value pane on the main chart, named after the script. Use `plot(value)` for bars along subscribed data or `set(key, value)` on a panel with an explicit axis. Null leaves a gap with `plot` or deletes the key with `set`. Multiple histograms on the same keyed panel form grouped bars. Stacking is not supported. `capacity` limits keys to 512 by default, with a maximum of 4096. Adding a new key when full stops the script with an error. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `horizontal` | `bool` | | `showLabel` | `bool` | | `showValue` | `bool` | | `capacity` | `int` | | `on` | `Surface?` | **Returns** `PlotHistogram` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.label {#plot-label} ```flowscope plot.label(title: string = "", color: color? = null, size: float? = null, offset: int = 0, on: Surface? = null, anchor: Anchor? = null) -> PlotLabel ``` Creates a plot of text labels. - `title` names the plot. - `color` and `size` set the text appearance. Null uses the theme default. `size` must be from 6.0 to 48.0, inclusive. - `offset` shifts values along the bars, from -500 to 500. Other values are rejected before the script runs. - `on` chooses the panel or pane. Null uses the main price pane. **Position** Leave `anchor` null to position each mark at its plotted value. Use `anchor.top` or `anchor.bottom` to pin it to that pane edge instead. The plotted value then sets the distance inward from the edge in logical pixels. Negative distances are allowed, but marks outside the pane are clipped. Edge-anchored marks do not affect the vertical scale or show a legend value. Other anchors are rejected before the script runs. Use `plot` to place labels along subscribed bars. For persistent text placed independently of a bar, use a label entity instead. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `size` | `float?` | | `offset` | `int` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `PlotLabel` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.line {#plot-line} ```flowscope plot.line(title: string = "", color: color? = null, width: float? = null, style: LineStyle? = null, area: bool = false, offset: int = 0, showLabel: bool = true, showValue: bool = true, capacity: int = 512, on: Surface? = null) -> PlotLine ``` Creates a line plot. - `title` names the plot. - `color`, `width`, and `style` set its appearance. Null uses the theme default. `width` must be from 0.0 to 10.0, inclusive. - `area` fills between the line and zero, using the line color. - `offset` shifts values along the bars, from -500 to 500. Other values are rejected before the script runs. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Null uses the main price pane. Use `plot(value)` for values along subscribed bars. On a panel with an explicit axis, use `set(key, value)`. Null leaves a gap with `plot` or deletes the key with `set`. `capacity` limits keys to 512 by default, with a maximum of 4096. Adding a new key when full stops the script with an error. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `width` | `float?` | | `style` | `LineStyle?` | | `area` | `bool` | | `offset` | `int` | | `showLabel` | `bool` | | `showValue` | `bool` | | `capacity` | `int` | | `on` | `Surface?` | **Returns** `PlotLine` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.marker {#plot-marker} ```flowscope plot.marker(title: string = "", color: color? = null, shape: Shape? = null, size: float? = null, borderWidth: float? = null, borderColor: color? = null, offset: int = 0, showLabel: bool = true, showValue: bool = true, on: Surface? = null, anchor: Anchor? = null) -> PlotMarker ``` Creates a plot of shaped markers. - `title` names the plot. - `color`, `shape`, `size`, `borderWidth`, and `borderColor` set the appearance. Null uses the theme default. `size` must be from 1.0 to 60.0 and `borderWidth` from 0.0 to 10.0, inclusive. - `offset` shifts values along the bars, from -500 to 500. Other values are rejected before the script runs. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Null uses the main price pane. **Position** Leave `anchor` null to position each mark at its plotted value. Use `anchor.top` or `anchor.bottom` to pin it to that pane edge instead. The plotted value then sets the distance inward from the edge in logical pixels. Negative distances are allowed, but marks outside the pane are clipped. Edge-anchored marks do not affect the vertical scale or show a legend value. Other anchors are rejected before the script runs. `showLabel` and `showValue` are ignored when anchored to an edge. Use `plot` along subscribed bars. Null skips a bar. For a panel with an explicit keyed axis, use a point plot instead. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `shape` | `Shape?` | | `size` | `float?` | | `borderWidth` | `float?` | | `borderColor` | `color?` | | `offset` | `int` | | `showLabel` | `bool` | | `showValue` | `bool` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `PlotMarker` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### plot.point {#plot-point} ```flowscope plot.point(title: string = "", color: color? = null, size: float? = null, showLabel: bool = true, showValue: bool = true, capacity: int = 512, on: Surface? = null) -> PlotPoint ``` Creates a scatter plot of dots. - `title` names the plot. - `color` and `size` set the dots' appearance. Null uses the theme default. `size` must be from 0.5 to 30.0, inclusive. Shape and border options are not supported. - `showLabel` and `showValue` control the legend label and value. - `on` chooses the panel or pane. Null uses the main price pane. Use `plot(value)` for dots along subscribed bars or `set(key, value)` on a panel with an explicit axis. Null skips a dot with `plot` or deletes the key with `set`. `capacity` limits keys to 512 by default, with a maximum of 4096. Adding a new key when full stops the script with an error. | Parameter | Type | |---|---| | `title` | `string` | | `color` | `color?` | | `size` | `float?` | | `showLabel` | `bool` | | `showValue` | `bool` | | `capacity` | `int` | | `on` | `Surface?` | **Returns** `PlotPoint` **Available in** Setup only: declarations, `setup` and `state` initialisers. ## Methods ### fill.fill {#fill-fill} ```flowscope fill.fill(color: color? = null, topValue: float? = null, bottomValue: float? = null, topColor: color? = null, bottomColor: color? = null) -> void ``` Fills between the two lines on the current bar. Pass `color` for a solid fill. For a vertical gradient, use `topValue` and `bottomValue` to set its positions and `topColor` and `bottomColor` for its colors. Do not mix solid and gradient colors in one call; this is rejected before the script runs. Null colors use the fill default. | Parameter | Type | |---|---| | `color` | `color?` | | `topValue` | `float?` | | `bottomValue` | `float?` | | `topColor` | `color?` | | `bottomColor` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.arrow.plot {#plot-arrow-plot} ```flowscope plot.arrow.plot(value: float?, colorUp: color? = null, colorDown: color? = null) -> void ``` Draws an arrow on the current bar. `value` sets the arrow's height and direction; null skips the arrow. `colorUp` and `colorDown` override the direction colors, with null keeping each plot default. The plot declaration sets the arrow's shape and size limits. | Parameter | Type | |---|---| | `value` | `float?` | | `colorUp` | `color?` | | `colorDown` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.bar.plot {#plot-bar-plot} ```flowscope plot.bar.plot(open: float?, high: float?, low: float?, close: float?, color: color? = null) -> void ``` Draws an OHLC bar on the current bar. `open`, `high`, `low`, and `close` set the bar prices. If any is null, the whole bar is skipped. `color` overrides its color; null keeps the plot default. | Parameter | Type | |---|---| | `open` | `float?` | | `high` | `float?` | | `low` | `float?` | | `close` | `float?` | | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.bg.plot {#plot-bg-plot} ```flowscope plot.bg.plot(color: color? = null) -> void ``` Tints the pane background on the bar being processed. `color` sets the tint for this bar, and null keeps the default. Calling `plot` marks the bar; not calling it leaves the bar clear. | Parameter | Type | |---|---| | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.candle.plot {#plot-candle-plot} ```flowscope plot.candle.plot(open: float?, high: float?, low: float?, close: float?, bodyColor: color? = null, wickColor: color? = null, borderColor: color? = null) -> void ``` Draws a candle on the current bar. `open`, `high`, `low`, and `close` set the candle prices. If any is null, the whole candle is skipped. `bodyColor`, `wickColor`, and `borderColor` override its colors. Null keeps each plot default. | Parameter | Type | |---|---| | `open` | `float?` | | `high` | `float?` | | `low` | `float?` | | `close` | `float?` | | `bodyColor` | `color?` | | `wickColor` | `color?` | | `borderColor` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.candle.set {#plot-candle-set} ```flowscope plot.candle.set(key: Key, open: float?, high: float?, low: float?, close: float?, bodyColor: color? = null, wickColor: color? = null, borderColor: color? = null) -> void ``` Adds or updates a candle at a time key. `key` sets the candle time. `open`, `high`, `low`, and `close` set its prices. If any price is null, the candle is deleted. `bodyColor`, `wickColor`, and `borderColor` override its colors. Null keeps each plot default. Reuse the same time key to update a custom candle while it forms. | Parameter | Type | |---|---| | `key` | `Key` | | `open` | `float?` | | `high` | `float?` | | `low` | `float?` | | `close` | `float?` | | `bodyColor` | `color?` | | `wickColor` | `color?` | | `borderColor` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.cells.cell {#plot-cells-cell} ```flowscope plot.cells.cell(from: float?, to: float?, weight: float? = null, color: color? = null, borderColor: color? = null, value: float? = null, textColor: color? = null, column: int = 0, align: Align = align.left, time: time? = null) -> void ``` Adds a cell to a bar. `from` and `to` are the cell's price range, in either order. Nothing happens when either is null or they are equal. - `weight` is the width, compared as the plot's `scale` says. Null fills the column. A weight of zero or less draws no fill; border and text still draw. - `color`, `borderColor` and `textColor` replace the plot's defaults. - `value` is written in the cell when it fits. - `column` chooses the column, from 0. A column the plot does not have stops the script with an error. - `align` is the edge the width grows from: `align.left`, `align.right`, or `align.center` for both ways. - `time` chooses the bar: null the current one, else the bar whose period contains it. A time after the current bar stops the script with an error. The first `cell` or `clear` for a bar in one calculation step discards what the bar held from earlier steps; further `cell` calls in the step add to it. A script whose bar can go from having cells to having none calls `clear`. | Parameter | Type | |---|---| | `from` | `float?` | | `to` | `float?` | | `weight` | `float?` | | `color` | `color?` | | `borderColor` | `color?` | | `value` | `float?` | | `textColor` | `color?` | | `column` | `int` | | `align` | `Align` | | `time` | `time?` | **Available in** Handlers and functions called from handlers. ### plot.cells.clear {#plot-cells-clear} ```flowscope plot.cells.clear(time: time? = null) -> void ``` Removes every cell this plot has on a bar. `time` chooses the bar as for `cell`: null the current one. `cell` calls after `clear` in the same calculation step add to the emptied bar. | Parameter | Type | |---|---| | `time` | `time?` | **Available in** Handlers and functions called from handlers. ### plot.histogram.plot {#plot-histogram-plot} ```flowscope plot.histogram.plot(value: float?, color: color? = null) -> void ``` Draws a histogram bar on the current bar. `value` sets the height from zero; null skips the bar. `color` overrides its color, with null keeping the plot default. | Parameter | Type | |---|---| | `value` | `float?` | | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.histogram.set {#plot-histogram-set} ```flowscope plot.histogram.set(key: Key, value: float?, color: color? = null) -> void ``` Adds or updates a histogram bar at a key. `key` sets the position on the panel's axis. `value` sets the height; null deletes the key. `color` overrides this bar's color, with null keeping the plot default. Category axes create bar charts; `horizontal` makes sideways bars. Multiple histograms on the same panel form grouped bars. Stacking is not supported. | Parameter | Type | |---|---| | `key` | `Key` | | `value` | `float?` | | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.label.plot {#plot-label-plot} ```flowscope plot.label.plot(price: float?, text: string, color: color? = null, size: float? = null) -> void ``` Draws a text label on the current bar. `price` sets the position, or the pixel distance from the pane edge when the plot uses an edge anchor. Null skips the label. `text` supplies the label text; several labels can use the same text. `color` and `size` override its appearance. Null keeps each plot default. A `size` outside its allowed range stops the script with an error. | Parameter | Type | |---|---| | `price` | `float?` | | `text` | `string` | | `color` | `color?` | | `size` | `float?` | **Available in** Handlers and functions called from handlers. ### plot.line.plot {#plot-line-plot} ```flowscope plot.line.plot(value: float?, color: color? = null) -> void ``` Draws a line value on the current bar. `value` sets the price; null leaves a gap. `color` overrides this point's color; null keeps the plot default. Values follow the panel's time axis: the main chart's timeframe on the main chart, or the panel's subscription timeframe in a window. A close handler places the value at the closing candle's end time. | Parameter | Type | |---|---| | `value` | `float?` | | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.line.set {#plot-line-set} ```flowscope plot.line.set(key: Key, value: float?, color: color? = null) -> void ``` Adds or updates a line value at a key. `key` sets the position on the panel's axis. `value` sets the price; null deletes the key. `color` overrides this point's color, with null keeping the plot default. Plots on the same panel share the axis. Use time keys for a custom time series. | Parameter | Type | |---|---| | `key` | `Key` | | `value` | `float?` | | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.marker.plot {#plot-marker-plot} ```flowscope plot.marker.plot(value: float?, color: color? = null, shape: Shape? = null, size: float? = null, borderWidth: float? = null, borderColor: color? = null) -> void ``` Draws a marker on the current bar. `value` sets the price, or the pixel distance from the pane edge when the plot uses an edge anchor. Null skips the marker. `color`, `shape`, `size`, `borderWidth`, and `borderColor` override its appearance. Null keeps each plot default. A `size` or `borderWidth` outside its allowed range stops the script with an error. | Parameter | Type | |---|---| | `value` | `float?` | | `color` | `color?` | | `shape` | `Shape?` | | `size` | `float?` | | `borderWidth` | `float?` | | `borderColor` | `color?` | **Available in** Handlers and functions called from handlers. ### plot.point.plot {#plot-point-plot} ```flowscope plot.point.plot(value: float?, color: color? = null, size: float? = null) -> void ``` Draws a dot on the current bar. `value` sets the price; null skips the dot. `color` and `size` override the dot's appearance, with null keeping each plot default. A `size` outside its allowed range stops the script with an error. Calling again on the same bar replaces the dot; the last call wins. | Parameter | Type | |---|---| | `value` | `float?` | | `color` | `color?` | | `size` | `float?` | **Available in** Handlers and functions called from handlers. ### plot.point.set {#plot-point-set} ```flowscope plot.point.set(key: Key, value: float?, color: color? = null, size: float? = null) -> void ``` Adds or updates a dot at a key. `key` sets the position on the panel's axis. `value` sets the price; null deletes the key. `color` and `size` override this dot's appearance, with null keeping each plot default. A `size` outside its allowed range stops the script with an error. Each key has one dot. Calling again with the same key replaces the dot. | Parameter | Type | |---|---| | `key` | `Key` | | `value` | `float?` | | `color` | `color?` | | `size` | `float?` | **Available in** Handlers and functions called from handlers. ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `extend.both` | `Extend` | `3` | Entity extends in both directions. | | `extend.down` | `Extend` | `5` | Line or box extends downward past its bottommost endpoint to the pane edge. | | `extend.left` | `Extend` | `1` | Entity extends leftward past its leftmost endpoint along its slope. | | `extend.none` | `Extend` | `0` | Entity does not extend beyond its x-range. | | `extend.right` | `Extend` | `2` | Entity extends rightward past its rightmost endpoint along its slope. | | `extend.up` | `Extend` | `4` | Line or box extends upward past its topmost endpoint to the pane edge. | | `extend.vertical` | `Extend` | `6` | Line or box extends both upward and downward to the pane edges. | | `linestyle.dashed` | `LineStyle` | `3` | Dashed line style. | | `linestyle.dotted` | `LineStyle` | `2` | Dotted line style. | | `linestyle.solid` | `LineStyle` | `1` | Solid line style. | | `linestyle.step` | `LineStyle` | `4` | Step line style. | | `cellcandles.overlay` | `CellCandles` | `0` | The chart's candles are drawn as usual, behind the cells. | | `cellcandles.side` | `CellCandles` | `1` | The chart's candles are drawn as a thin strip at the left edge of each bar, beside the cells. | | `cellcandles.hidden` | `CellCandles` | `2` | The chart's candles are not drawn. | | `cellformat.compact` | `CellFormat` | `0` | Large numbers are shortened with a suffix, such as 3.21K or 53.1M. | | `cellformat.fixed` | `CellFormat` | `1` | The number is written in full, such as 3214.56. | | `cellformat.percent` | `CellFormat` | `2` | The value is a ratio written as a percentage, so 0.153 reads 15.3%. | | `cellscale.bar` | `CellScale` | `0` | A full-width cell has the largest weight on its own bar. | | `cellscale.view` | `CellScale` | `1` | A full-width cell has the largest weight on the visible bars. | | `cellscale.unit` | `CellScale` | `2` | The weight is the width itself, as a fraction from 0 to 1. | | `shape.asterisk` | `Shape` | `10` | Asterisk marker shape. | | `shape.circle` | `Shape` | `1` | Circle marker shape. | | `shape.cross` | `Shape` | `8` | Cross marker shape. | | `shape.diamond` | `Shape` | `3` | Diamond marker shape. | | `shape.down` | `Shape` | `5` | Down-triangle marker shape. | | `shape.left` | `Shape` | `6` | Left-triangle marker shape. | | `shape.plus` | `Shape` | `9` | Plus marker shape. | | `shape.right` | `Shape` | `7` | Right-triangle marker shape. | | `shape.square` | `Shape` | `2` | Square marker shape. | | `shape.up` | `Shape` | `4` | Up-triangle marker shape. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/plot --- # surfaces reference > Panes, windows and their chart, table and heatmap panels. The price pane is implicit. `pane(...)` adds a pane below the chart sharing its time axis; `window(...)` declares a separate window with chart, table or heatmap panels. ## Functions ### chart.panel {#chart-panel} ```flowscope chart.panel(sub: Subscription? = null, axis: Axis? = null, title: string = "") -> ChartPanel ``` Adds a chart panel to a window. Declare it as a named field inside a `window` block, then refer to it as `win.main`. Choose one data layout: - `sub`: Use a bar subscription to set the chart's horizontal axis. - `axis`: Choose an axis and add plot values with `set(key, ...)`. Each key must match the axis's type. Provide either `sub` or `axis`, but not both. Leaving both null is also an error. Use `title` to name the panel. Set `on: win.main` to draw in its price pane. To draw in a separate pane, create one with `pane(..., on: win.main)` and target that pane instead. | Parameter | Type | |---|---| | `sub` | `Subscription?` | | `axis` | `Axis?` | | `title` | `string` | **Returns** `ChartPanel` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### heatmap.new {#heatmap-new} ```flowscope heatmap.new(title: string = "", x: Axis, y: Axis, capacity: int, min: float? = null, max: float? = null, palette: color[] = []) -> HeatmapPanel ``` Creates a heatmap panel. - `title` names the panel. - `x` and `y` choose the two axes and the types of keys used for each. - `capacity` limits the number of cells, from 1 to 65536. Other values are rejected before the script runs. When the heatmap is full, a new cell drops the oldest one, so a heatmap keyed by bar scrolls. - `min` and `max` set a fixed color scale when both are supplied. Leave them null for automatic scaling. - `palette` supplies the gradient colors. An empty list uses the default palette. Add or update cells with `hm.set(x, y, value)`. | Parameter | Type | |---|---| | `title` | `string` | | `x` | `Axis` | | `y` | `Axis` | | `capacity` | `int` | | `min` | `float?` | | `max` | `float?` | | `palette` | `color[]` | **Returns** `HeatmapPanel` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### pane {#pane} ```flowscope pane(title: string = "", height: float = 0.3, on: ChartPanel? = null) -> Pane ``` Adds a separate pane to a chart. - `title` names the pane. - `height` sets its size relative to the main price pane: `0.5` means half as tall. Use a value from 0.05 to 1.0, inclusive. Other values are rejected before the script runs. - `on` chooses a window chart panel, such as `dash.daily`. Omit it to use the main chart. Use `on: paneName` when declaring a plot to draw in this pane. Plots without `on:` use their default pane. Panes appear in declaration order, with up to 4 per chart. Give the pane a name and keep it stable to preserve saved appearance settings. Moving an unnamed pane to another declaration can reset those settings. Names starting with `$` are reserved for panes created automatically by the app. | Parameter | Type | |---|---| | `title` | `string` | | `height` | `float` | | `on` | `ChartPanel?` | **Returns** `Pane` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### table.cell {#table-cell} ```flowscope table.cell(value: scalar, fg: color? = null, bg: color? = null, align: Align? = null) -> TableCell ``` Adds text color, background color, or alignment to a table value. `value` can be a float, int, bool, string, or time. Null displays an empty cell. - `fg` sets the text color. - `bg` sets the background color. - `align` sets text alignment. Leave a style option null to use the table or column setting. For unstyled cells, pass values directly to `row` or `set`; no `table.cell` wrapper is needed. | Parameter | Type | |---|---| | `value` | `scalar` | | `fg` | `color?` | | `bg` | `color?` | | `align` | `Align?` | **Returns** `TableCell` **Available in** Handlers and functions called from handlers. ### table.col {#table-col} ```flowscope table.col(title: string = "", align: Align? = null, width: float = 1) -> TableColumn ``` Defines a column for a table. Use this inside the `columns` list of `table.new`. - `title` names the column. - `align` sets text alignment. Null uses left alignment. - `width` sets the column's relative width. A value of `0.6` gives it 60% of the space given to a column with width `1.0`. Use 0.1 to 10.0, inclusive; other values are rejected before the script runs. | Parameter | Type | |---|---| | `title` | `string` | | `align` | `Align?` | | `width` | `float` | **Returns** `TableColumn` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### table.new {#table-new} ```flowscope table.new(columns: TableColumn[], title: string = "", key: Axis? = null, capacity: int = 100) -> TablePanel ``` Creates a table panel with fixed columns. - `title` names the panel. - `columns` defines up to 16 columns using `table.col`. - `key` chooses the row-key type. Leave it null to use string category keys. - `capacity` limits the number of rows. Values outside the allowed range are rejected before the script runs. Adding a new row when full stops the script with an error. Rows stay in the table until you update or delete them. Use the table methods to add rows, change cells, or clear data. The app handles sorting, filtering, and resizing columns. | Parameter | Type | |---|---| | `title` | `string` | | `columns` | `TableColumn[]` | | `key` | `Axis?` | | `capacity` | `int` | **Returns** `TablePanel` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### window {#window} ```flowscope window(title: string = "") -> Window ``` Creates a window containing charts, heatmaps, or tables. `title` names the window. A script attached to a chart starts with its window closed; open it from the badge beside the script's name. A script opened without a chart shows its window immediately. Declare each panel as a named field inside the window block, then refer to it as `win.main`. Each field needs a different name within its window and must contain one chart, heatmap, or table panel. A panel cannot be shared between fields. Different windows can reuse field names. Use `on: win.main` to add a pane to a window's chart panel. Keep the window and panel names stable to preserve saved settings; `title` only changes the label. A script can create up to 4 windows. | Parameter | Type | |---|---| | `title` | `string` | **Returns** `Window` **Available in** Setup only: declarations, `setup` and `state` initialisers. ## Methods ### surfaces.heatmap.set {#surfaces-heatmap-set} ```flowscope surfaces.heatmap.set(x: Key, y: Key, value: float?) -> void ``` Adds or updates a heatmap cell. `x` and `y` locate the cell. `value` sets its value; null deletes the cell. The panel's color scale determines the cell color. Adding a new cell when `capacity` is reached stops the script with an error. | Parameter | Type | |---|---| | `x` | `Key` | | `y` | `Key` | | `value` | `float?` | **Available in** Handlers and functions called from handlers. ### surfaces.pane.setTitle {#surfaces-pane-settitle} ```flowscope surfaces.pane.setTitle(text: string) -> void ``` Sets the title the chart shows for this pane's legend row. Call this on a pane you declared, such as `wt.setTitle(text)`, or another name referring to that pane. You can call it on every update. The chart refreshes the title after the handler finishes when the text has changed. Pass an empty string to restore the pane's original `title:`. Titles are limited to 256 UTF-8 bytes; longer text is shortened. Saved appearance settings keep using the pane's declared name. Use `script.setTitle` to change the title on the main price pane. | Parameter | Type | |---|---| | `text` | `string` | **Available in** Handlers and functions called from handlers. ### surfaces.table.clear {#surfaces-table-clear} ```flowscope surfaces.table.clear(key: Key? = null) -> void ``` Deletes one row or clears the table. Pass `key` to delete that row. An unknown key does nothing. Omit `key`, or pass null, to delete every row. | Parameter | Type | |---|---| | `key` | `Key?` | **Available in** Handlers and functions called from handlers. ### surfaces.table.row {#surfaces-table-row} ```flowscope surfaces.table.row(key: Key, cells: TableCell...) -> void ``` Adds or replaces a whole table row. `key` identifies the row. Supply one value in `cells` for each column, in column order. The number of cells must match the column count. A mismatch is rejected before the script runs when it can be checked then; otherwise it stops the script with an error. Pass plain values for unstyled cells or use `table.cell` to style individual cells. Use `rowBg` to set the row's background. Updating a row replaces all of its cells together. | Parameter | Type | |---|---| | `key` | `Key` | | `cells` | `TableCell` | **Available in** Handlers and functions called from handlers. ### surfaces.table.rowBg {#surfaces-table-rowbg} ```flowscope surfaces.table.rowBg(key: Key, bg: color?) -> void ``` Changes the background color of an existing row. `key` identifies the row. `bg` sets its background color; null restores the theme default. A missing row stops the script with an error. Use this after `row` to style the whole row. | Parameter | Type | |---|---| | `key` | `Key` | | `bg` | `color?` | **Available in** Handlers and functions called from handlers. ### surfaces.table.set {#surfaces-table-set} ```flowscope surfaces.table.set(key: Key, column: int, value: TableCell) -> void ``` Changes one cell in an existing row. `key` identifies the row. `column` selects the column, counting from 0. `value` can be a plain value or a styled `table.cell`. A missing row, a negative `column`, or a column at or beyond the column count stops the script with an error. Use `row` to create a row first. | Parameter | Type | |---|---| | `key` | `Key` | | `column` | `int` | | `value` | `TableCell` | **Available in** Handlers and functions called from handlers. ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `align.center` | `Align` | `1` | Centers text. | | `align.left` | `Align` | `0` | Aligns text to the left. This is the default for columns and cells. | | `align.right` | `Align` | `2` | Aligns text to the right. | | `axis.category` | `Axis` | `2` | Uses string labels as keys, for example for bars grouped by exchange or symbol. | | `axis.index` | `Axis` | `1` | Uses integer positions as keys, for example for distributions. | | `axis.time` | `Axis` | `0` | Uses time values as keys, for example for custom candles or session bars. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/surfaces --- # entities reference > Keyed drawings: lines, boxes, labels, arrows and more. Drawings are anchored to time and price. Each kind lives in a pool addressed by key: setting the same key again updates the drawing instead of adding one. ## Functions ### entities.arrowPool {#entities-arrowpool} ```flowscope entities.arrowPool(max: int, on: Surface? = null, anchor: Anchor? = null) -> ArrowPool ``` Creates a pool of arrows with a fixed limit. Declare the pool with `setup` or `state`, then use it from any handler. Each drawing is identified by its keys. - `max` limits the number of arrows. Values outside the entity-pool limit are rejected before the script runs. Creating a new drawing when full removes the oldest one. - `on` chooses the panel or pane for every drawing in the pool. Null uses the main price pane. - `anchor` chooses how to position drawings. Null uses chart coordinates. **Anchors** A pane anchor, such as `anchor.topLeft` or `anchor.center`, measures both coordinates in logical pixels from that point. An edge anchor, such as `anchor.top` or `anchor.left`, measures one coordinate inward from the edge in pixels and keeps the other in chart coordinates. | Parameter | Type | |---|---| | `max` | `int` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `ArrowPool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### entities.boxPool {#entities-boxpool} ```flowscope entities.boxPool(max: int, on: Surface? = null, anchor: Anchor? = null) -> BoxPool ``` Creates a pool of boxes with a fixed limit. Declare the pool with `setup` or `state`, then use it from any handler. Each drawing is identified by its keys. - `max` limits the number of boxes. Values outside the entity-pool limit are rejected before the script runs. Creating a new drawing when full removes the oldest one. - `on` chooses the panel or pane for every drawing in the pool. Null uses the main price pane. - `anchor` chooses how to position drawings. Null uses chart coordinates. **Anchors** A pane anchor, such as `anchor.topLeft` or `anchor.center`, measures both coordinates in logical pixels from that point. An edge anchor, such as `anchor.top` or `anchor.left`, measures one coordinate inward from the edge in pixels and keeps the other in chart coordinates. | Parameter | Type | |---|---| | `max` | `int` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `BoxPool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### entities.labelPool {#entities-labelpool} ```flowscope entities.labelPool(max: int, on: Surface? = null, anchor: Anchor? = null) -> LabelPool ``` Creates a pool of labels with a fixed limit. Declare the pool with `setup` or `state`, then use it from any handler. Each drawing is identified by its keys. - `max` limits the number of labels. Values outside the entity-pool limit are rejected before the script runs. Creating a new drawing when full removes the oldest one. - `on` chooses the panel or pane for every drawing in the pool. Null uses the main price pane. - `anchor` chooses how to position drawings. Null uses chart coordinates. **Anchors** A pane anchor, such as `anchor.topLeft` or `anchor.center`, measures both coordinates in logical pixels from that point. An edge anchor, such as `anchor.top` or `anchor.left`, measures one coordinate inward from the edge in pixels and keeps the other in chart coordinates. | Parameter | Type | |---|---| | `max` | `int` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `LabelPool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### entities.lineFillPool {#entities-linefillpool} ```flowscope entities.lineFillPool(max: int, on: Surface? = null) -> LineFillPool ``` Creates a pool of line fills with a fixed limit. Declare the pool with `setup` or `state`, then use it from any handler. Each drawing is identified by its keys. - `max` limits the number of line fills. Values outside the entity-pool limit are rejected before the script runs. Creating a new drawing when full removes the oldest one. - `on` chooses the panel or pane for every drawing in the pool. Null uses the main price pane. | Parameter | Type | |---|---| | `max` | `int` | | `on` | `Surface?` | **Returns** `LineFillPool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### entities.linePool {#entities-linepool} ```flowscope entities.linePool(max: int, on: Surface? = null, anchor: Anchor? = null) -> LinePool ``` Creates a pool of lines with a fixed limit. Declare the pool with `setup` or `state`, then use it from any handler. Each drawing is identified by its keys. - `max` limits the number of lines. Values outside the entity-pool limit are rejected before the script runs. Creating a new drawing when full removes the oldest one. - `on` chooses the panel or pane for every drawing in the pool. Null uses the main price pane. - `anchor` chooses how to position drawings. Null uses chart coordinates. **Anchors** A pane anchor, such as `anchor.topLeft` or `anchor.center`, measures both coordinates in logical pixels from that point. An edge anchor, such as `anchor.top` or `anchor.left`, measures one coordinate inward from the edge in pixels and keeps the other in chart coordinates. | Parameter | Type | |---|---| | `max` | `int` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `LinePool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ### entities.markerPool {#entities-markerpool} ```flowscope entities.markerPool(max: int, on: Surface? = null, anchor: Anchor? = null) -> MarkerPool ``` Creates a pool of markers with a fixed limit. Declare the pool with `setup` or `state`, then use it from any handler. Each drawing is identified by its keys. - `max` limits the number of markers. Values outside the entity-pool limit are rejected before the script runs. Creating a new drawing when full removes the oldest one. - `on` chooses the panel or pane for every drawing in the pool. Null uses the main price pane. - `anchor` chooses how to position drawings. Null uses chart coordinates. **Anchors** A pane anchor, such as `anchor.topLeft` or `anchor.center`, measures both coordinates in logical pixels from that point. An edge anchor, such as `anchor.top` or `anchor.left`, measures one coordinate inward from the edge in pixels and keeps the other in chart coordinates. | Parameter | Type | |---|---| | `max` | `int` | | `on` | `Surface?` | | `anchor` | `Anchor?` | **Returns** `MarkerPool` **Available in** Setup only: declarations, `setup` and `state` initialisers. ## Methods ### entities.arrow.set {#entities-arrow-set} ```flowscope entities.arrow.set(x1: time|int|float, y1: float, x2: time|int|float, y2: float, width: float = 0, color: color? = null, style: LineStyle? = null, extend: Extend? = null, headLengthPx: float = 0, headWidthRatio: float = 0, zIndex: int = 0) -> void ``` Draws or updates an arrow. `x1`, `y1`, `x2`, and `y2` set the endpoints. - `width` accepts 0.0 to 20.0, defaulting to 0.0. - `color` and `style` use chart defaults when null. - `extend` continues the arrow along its slope toward a pane edge. Use `extend.left`, `extend.right`, or `extend.both`; null leaves it unextended. - `headLengthPx` accepts 0.0 to 200.0 pixels, defaulting to 0.0. - `headWidthRatio` accepts 0.0 to 20.0, defaulting to 0.0. - `zIndex` sets the drawing order and defaults to 0. A width, head length, or head width ratio outside its range stops the script with an error. **Coordinates** The pool's anchor determines how coordinates are measured: - No anchor: use chart coordinates; x can be a time value. - A pane anchor, such as `anchor.topLeft`: use finite logical-pixel offsets for both coordinates. Time values are not allowed. - `anchor.top` or `anchor.bottom`: use chart x coordinates and finite pixel y offsets. - `anchor.left` or `anchor.right`: use finite pixel x offsets and chart y coordinates. Arrows support horizontal extensions only. The pool must use chart x coordinates, either without an anchor or with a top/bottom edge anchor. Other pools accept only null or `extend.none`. Vertical extensions are not allowed. | Parameter | Type | |---|---| | `x1` | `time\|int\|float` | | `y1` | `float` | | `x2` | `time\|int\|float` | | `y2` | `float` | | `width` | `float` | | `color` | `color?` | | `style` | `LineStyle?` | | `extend` | `Extend?` | | `headLengthPx` | `float` | | `headWidthRatio` | `float` | | `zIndex` | `int` | **Available in** Handlers and functions called from handlers. ### entities.arrowPool.clear {#entities-arrowpool-clear} ```flowscope entities.arrowPool.clear(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Clears arrows by key prefix. Removes all arrows whose keys start with `key` and any additional `keys`, both from the pool and the chart. Returns true if at least one arrow was removed. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.arrowPool.delete {#entities-arrowpool-delete} ```flowscope entities.arrowPool.delete(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Deletes an arrow. Removes the arrow for `key` and any additional `keys` from the pool and the chart. Returns true if it was removed, or false if those keys were not found. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.arrowPool.get {#entities-arrowpool-get} ```flowscope entities.arrowPool.get(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> Arrow ``` Returns an arrow for the given keys. Returns the arrow for `key` and any additional `keys`, creating one if needed. The result is never null. If the pool is full, the oldest arrow is removed. Call `set` to draw the new arrow. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `Arrow` **Available in** Handlers and functions called from handlers. ### entities.arrowPool.has {#entities-arrowpool-has} ```flowscope entities.arrowPool.has(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Tests whether an arrow exists. Returns true if the pool contains an arrow for `key` and any additional `keys`. This does not create, change, or draw anything. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.box.set {#entities-box-set} ```flowscope entities.box.set(x1: time|int|float, y1: float, x2: time|int|float, y2: float, rounding: float = 0, color: color? = null, borderWidth: float = 0, borderColor: color? = null, borderStyle: LineStyle? = null, extend: Extend? = null, text: string = "", textColor: color? = null, textSize: float = 0, font: FontFamily? = null, textHAlign: string? = null, textVAlign: string? = null, textWrap: bool? = null, zIndex: int = 0) -> void ``` Draws or updates a box. `x1`, `y1`, `x2`, and `y2` set the corners. - `rounding` rounds the corners and defaults to 0.0. Negative values stop the script with an error. - `color` sets the fill color and defaults to null. - `borderWidth` accepts 0.0 to 20.0, defaulting to 0.0. Other values stop the script with an error. `borderColor` and `borderStyle` default to null. - `extend` stretches the box to pane edges: `extend.left`, `extend.right`, `extend.both`, `extend.up`, `extend.down`, or `extend.vertical`. Null leaves the box unextended. - `text` adds a label, empty by default. `textColor` defaults to null. - `textSize` accepts 0.0 to 96.0, defaulting to 0.0. Other values stop the script with an error. `font` defaults to `font.app` when null. - `textHAlign`, `textVAlign`, and `textWrap` set text layout and default to null. - `zIndex` sets the drawing order and defaults to 0. **Coordinates** The pool's anchor determines how coordinates are measured: - No anchor: use chart coordinates; x can be a time value. - A pane anchor, such as `anchor.topLeft`: use finite logical-pixel offsets for both coordinates. Time values are not allowed. - `anchor.top` or `anchor.bottom`: use chart x coordinates and finite pixel y offsets. - `anchor.left` or `anchor.right`: use finite pixel x offsets and chart y coordinates. Horizontal extensions need chart x coordinates: use a pool with no anchor or with a top/bottom edge anchor. Vertical extensions need chart y coordinates: use a pool with no anchor or with a left/right edge anchor. A pool using pixels for both coordinates accepts only null or `extend.none`. | Parameter | Type | |---|---| | `x1` | `time\|int\|float` | | `y1` | `float` | | `x2` | `time\|int\|float` | | `y2` | `float` | | `rounding` | `float` | | `color` | `color?` | | `borderWidth` | `float` | | `borderColor` | `color?` | | `borderStyle` | `LineStyle?` | | `extend` | `Extend?` | | `text` | `string` | | `textColor` | `color?` | | `textSize` | `float` | | `font` | `FontFamily?` | | `textHAlign` | `string?` | | `textVAlign` | `string?` | | `textWrap` | `bool?` | | `zIndex` | `int` | **Available in** Handlers and functions called from handlers. ### entities.boxPool.clear {#entities-boxpool-clear} ```flowscope entities.boxPool.clear(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Removes boxes whose keys start with the given keys. Removes all boxes whose keys start with `key` and any additional `keys`, both from the pool and the chart. Returns true if at least one box was removed. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.boxPool.delete {#entities-boxpool-delete} ```flowscope entities.boxPool.delete(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Deletes a box. Removes the box for `key` and any additional `keys` from the pool and the chart. Returns true if it was removed, or false if those keys were not found. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.boxPool.get {#entities-boxpool-get} ```flowscope entities.boxPool.get(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> Box ``` Returns a box for the given keys. Returns the box for `key` and any additional `keys`, creating one if needed. The result is never null. If the pool is full, the oldest box is removed. Call `set` to draw the new box. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `Box` **Available in** Handlers and functions called from handlers. ### entities.boxPool.has {#entities-boxpool-has} ```flowscope entities.boxPool.has(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Tests whether a box exists. Returns true if the pool contains a box for `key` and any additional `keys`. This does not create, change, or draw anything. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.label.set {#entities-label-set} ```flowscope entities.label.set(x: time|int|float, y: float, text: string, color: color? = null, size: float = 0, font: FontFamily? = null, textAlign: string? = null, zIndex: int = 0) -> void ``` Draws or updates a text label. `x` and `y` set the position. The pool's anchor also selects which point on the label is placed at that position. `text` supplies the label text. - `color` uses the chart default when null. - `size` accepts 0.0 to 96.0, defaulting to 0.0. Other values stop the script with an error. - `font` defaults to `font.app` when null. `textAlign` defaults to null. - `zIndex` sets the drawing order and defaults to 0. **Coordinates** The pool's anchor determines how coordinates are measured: - No anchor: use chart coordinates; x can be a time value. - A pane anchor, such as `anchor.topLeft`: use finite logical-pixel offsets for both coordinates. Time values are not allowed. - `anchor.top` or `anchor.bottom`: use chart x coordinates and finite pixel y offsets. - `anchor.left` or `anchor.right`: use finite pixel x offsets and chart y coordinates. | Parameter | Type | |---|---| | `x` | `time\|int\|float` | | `y` | `float` | | `text` | `string` | | `color` | `color?` | | `size` | `float` | | `font` | `FontFamily?` | | `textAlign` | `string?` | | `zIndex` | `int` | **Available in** Handlers and functions called from handlers. ### entities.labelPool.clear {#entities-labelpool-clear} ```flowscope entities.labelPool.clear(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Clears labels by key prefix. Removes all labels whose keys start with `key` and any additional `keys`, both from the pool and the chart. Returns true if at least one label was removed. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.labelPool.delete {#entities-labelpool-delete} ```flowscope entities.labelPool.delete(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Deletes a label. Removes the label for `key` and any additional `keys` from the pool and the chart. Returns true if it was removed, or false if those keys were not found. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.labelPool.get {#entities-labelpool-get} ```flowscope entities.labelPool.get(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> Label ``` Returns a label for the given keys. Returns the label for `key` and any additional `keys`, creating one if needed. The result is never null. If the pool is full, the oldest label is removed. Call `set` to draw the new label. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `Label` **Available in** Handlers and functions called from handlers. ### entities.labelPool.has {#entities-labelpool-has} ```flowscope entities.labelPool.has(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Tests whether a label exists. Returns true if the pool contains a label for `key` and any additional `keys`. This does not create, change, or draw anything. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.line.set {#entities-line-set} ```flowscope entities.line.set(x1: time|int|float, y1: float, x2: time|int|float, y2: float, width: float = 0, color: color? = null, style: LineStyle? = null, extend: Extend? = null, text: string = "", font: FontFamily? = null, zIndex: int = 0, axisLabel: bool = false) -> void ``` Draws or updates a line. `x1`, `y1`, `x2`, and `y2` set the endpoints. - `width` accepts 0.0 to 20.0, defaulting to 0.0. Other values stop the script with an error. - `color` and `style` use chart defaults when null. - `extend` continues the line toward the pane edges. Use `extend.left`, `extend.right`, or `extend.both` to follow its slope horizontally, or `extend.up`, `extend.down`, or `extend.vertical` to extend vertically. Null leaves the line unextended. - `text` adds a label, empty by default. `font` defaults to `font.app` when null. - `zIndex` sets the drawing order and defaults to 0. - `axisLabel` puts the line's price at its right end on the price scale, in the line's colour, as an indicator's value tag. Off by default. **Coordinates** The pool's anchor determines how coordinates are measured: - No anchor: use chart coordinates; x can be a time value. - A pane anchor, such as `anchor.topLeft`: use finite logical-pixel offsets for both coordinates. Time values are not allowed. - `anchor.top` or `anchor.bottom`: use chart x coordinates and finite pixel y offsets. - `anchor.left` or `anchor.right`: use finite pixel x offsets and chart y coordinates. Horizontal extensions need chart x coordinates: use a pool with no anchor or with a top/bottom edge anchor. Vertical extensions need chart y coordinates: use a pool with no anchor or with a left/right edge anchor. A pool using pixels for both coordinates accepts only null or `extend.none`. A horizontal line with a vertical extension spans the full visible width. | Parameter | Type | |---|---| | `x1` | `time\|int\|float` | | `y1` | `float` | | `x2` | `time\|int\|float` | | `y2` | `float` | | `width` | `float` | | `color` | `color?` | | `style` | `LineStyle?` | | `extend` | `Extend?` | | `text` | `string` | | `font` | `FontFamily?` | | `zIndex` | `int` | | `axisLabel` | `bool` | **Available in** Handlers and functions called from handlers. ### entities.lineFill.set {#entities-linefill-set} ```flowscope entities.lineFill.set(lineA: Line, lineB: Line, color: color? = null) -> void ``` Draws or updates a fill between two lines. `lineA` and `lineB` must be existing, drawn lines on the same pane. Both must use the same kind of positioning: chart coordinates, offsets from a pane point, or offsets from an edge. Their individual anchors may differ, such as one line anchored to the top and another to the bottom. The fill follows both lines as they appear on the pane. `color` uses the chart default when null. Deleting either line also removes the fill; a null line cannot be used as a boundary. | Parameter | Type | |---|---| | `lineA` | `Line` | | `lineB` | `Line` | | `color` | `color?` | **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.clear {#entities-linefillpool-clear} ```flowscope entities.lineFillPool.clear(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Clears line fills by key prefix. Removes all line fills whose keys start with `key` and any additional `keys`, both from the pool and the chart. Returns true if at least one line fill was removed. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.delete {#entities-linefillpool-delete} ```flowscope entities.lineFillPool.delete(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Deletes a line fill. Removes the line fill for `key` and any additional `keys` from the pool and the chart. Returns true if it was removed, or false if those keys were not found. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.get {#entities-linefillpool-get} ```flowscope entities.lineFillPool.get(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> LineFill ``` Returns a line fill for the given keys. Returns the line fill for `key` and any additional `keys`, creating one if needed. The result is never null. If the pool is full, the oldest line fill is removed. Call `set` to draw the new line fill. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `LineFill` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.has {#entities-linefillpool-has} ```flowscope entities.lineFillPool.has(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Tests whether a line fill exists. Returns true if the pool contains a line fill for `key` and any additional `keys`. This does not create, change, or draw anything. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.linePool.clear {#entities-linepool-clear} ```flowscope entities.linePool.clear(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Clears lines by key prefix. Removes all lines whose keys start with `key` and any additional `keys`, both from the pool and the chart. Returns true if at least one line was removed. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.linePool.delete {#entities-linepool-delete} ```flowscope entities.linePool.delete(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Deletes a line. Removes the line for `key` and any additional `keys` from the pool and the chart. Returns true if it was removed, or false if those keys were not found. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.linePool.get {#entities-linepool-get} ```flowscope entities.linePool.get(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> Line ``` Returns a line for the given keys. Returns the line for `key` and any additional `keys`, creating one if needed. The result is never null. If the pool is full, the oldest line is removed. Call `set` to draw the new line. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `Line` **Available in** Handlers and functions called from handlers. ### entities.linePool.has {#entities-linepool-has} ```flowscope entities.linePool.has(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Tests whether a line exists. Returns true if the pool contains a line for `key` and any additional `keys`. This does not create, change, or draw anything. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.marker.set {#entities-marker-set} ```flowscope entities.marker.set(x: time|int|float, y: float, shape: Shape? = null, color: color? = null, size: float = 0, zIndex: int = 0) -> void ``` Draws or updates a marker. `x` and `y` set the position. - `shape` and `color` use chart defaults when null. - `size` accepts 0.0 to 96.0, defaulting to 0.0. Other values stop the script with an error. - `zIndex` sets the drawing order and defaults to 0. **Coordinates** The pool's anchor determines how coordinates are measured: - No anchor: use chart coordinates; x can be a time value. - A pane anchor, such as `anchor.topLeft`: use finite logical-pixel offsets for both coordinates. Time values are not allowed. - `anchor.top` or `anchor.bottom`: use chart x coordinates and finite pixel y offsets. - `anchor.left` or `anchor.right`: use finite pixel x offsets and chart y coordinates. | Parameter | Type | |---|---| | `x` | `time\|int\|float` | | `y` | `float` | | `shape` | `Shape?` | | `color` | `color?` | | `size` | `float` | | `zIndex` | `int` | **Available in** Handlers and functions called from handlers. ### entities.markerPool.clear {#entities-markerpool-clear} ```flowscope entities.markerPool.clear(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Clears markers by key prefix. Removes all markers whose keys start with `key` and any additional `keys`, both from the pool and the chart. Returns true if at least one marker was removed. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.markerPool.delete {#entities-markerpool-delete} ```flowscope entities.markerPool.delete(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Deletes a marker. Removes the marker for `key` and any additional `keys` from the pool and the chart. Returns true if it was removed, or false if those keys were not found. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.markerPool.get {#entities-markerpool-get} ```flowscope entities.markerPool.get(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> Marker ``` Returns a marker for the given keys. Returns the marker for `key` and any additional `keys`, creating one if needed. The result is never null. If the pool is full, the oldest marker is removed. Call `set` to draw the new marker. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `Marker` **Available in** Handlers and functions called from handlers. ### entities.markerPool.has {#entities-markerpool-has} ```flowscope entities.markerPool.has(key: int|time|duration|float|string, keys: int|time|duration|float|string...) -> bool ``` Tests whether a marker exists. Returns true if the pool contains a marker for `key` and any additional `keys`. This does not create, change, or draw anything. | Parameter | Type | |---|---| | `key` | `int\|time\|duration\|float\|string` | | `keys` | `int\|time\|duration\|float\|string` | **Returns** `bool` **Available in** Handlers and functions called from handlers. ## Properties ### entities.arrowPool.capacity {#entities-arrowpool-capacity} ```flowscope entities.arrowPool.capacity: int ``` Returns the maximum number of arrows in the pool. Returns the `max` value accepted for the arrow pool at setup. The capacity does not change during the run. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.arrowPool.isEmpty {#entities-arrowpool-isempty} ```flowscope entities.arrowPool.isEmpty: bool ``` Reports whether the arrow pool has no drawings. Returns true when the pool contains no arrows. An arrow created with `get` counts even before it is drawn with `set`. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.arrowPool.isFull {#entities-arrowpool-isfull} ```flowscope entities.arrowPool.isFull: bool ``` Reports whether the arrow pool is at capacity. Returns true when the pool has reached its `max` count. You can still call `get` with new keys; doing so removes the oldest arrow. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.arrowPool.len {#entities-arrowpool-len} ```flowscope entities.arrowPool.len: int ``` Counts the arrows currently in the pool. Returns the number of arrows currently in the pool, including any created with `get` that have not yet been drawn. Removed arrows are not counted. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.boxPool.capacity {#entities-boxpool-capacity} ```flowscope entities.boxPool.capacity: int ``` Returns the maximum number of boxes in the pool. Returns the `max` value accepted for the box pool at setup. The capacity does not change during the run. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.boxPool.isEmpty {#entities-boxpool-isempty} ```flowscope entities.boxPool.isEmpty: bool ``` Reports whether the box pool has no drawings. Returns true when the pool contains no boxes. A box created with `get` counts even before it is drawn with `set`. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.boxPool.isFull {#entities-boxpool-isfull} ```flowscope entities.boxPool.isFull: bool ``` Reports whether the box pool is at capacity. Returns true when the pool has reached its `max` count. You can still call `get` with new keys; doing so removes the oldest box. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.boxPool.len {#entities-boxpool-len} ```flowscope entities.boxPool.len: int ``` Counts the boxes currently in the pool. Returns the number of boxes currently in the pool, including any created with `get` that have not yet been drawn. Removed boxes are not counted. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.labelPool.capacity {#entities-labelpool-capacity} ```flowscope entities.labelPool.capacity: int ``` Returns the maximum number of labels in the pool. Returns the `max` value accepted for the label pool at setup. The capacity does not change during the run. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.labelPool.isEmpty {#entities-labelpool-isempty} ```flowscope entities.labelPool.isEmpty: bool ``` Reports whether the label pool has no drawings. Returns true when the pool contains no labels. A label created with `get` counts even before it is drawn with `set`. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.labelPool.isFull {#entities-labelpool-isfull} ```flowscope entities.labelPool.isFull: bool ``` Reports whether the label pool is at capacity. Returns true when the pool has reached its `max` count. You can still call `get` with new keys; doing so removes the oldest label. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.labelPool.len {#entities-labelpool-len} ```flowscope entities.labelPool.len: int ``` Counts the labels currently in the pool. Returns the number of labels currently in the pool, including any created with `get` that have not yet been drawn. Removed labels are not counted. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.capacity {#entities-linefillpool-capacity} ```flowscope entities.lineFillPool.capacity: int ``` Returns the maximum number of line fills in the pool. Returns the `max` value accepted for the line fill pool at setup. The capacity does not change during the run. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.isEmpty {#entities-linefillpool-isempty} ```flowscope entities.lineFillPool.isEmpty: bool ``` Reports whether the line fill pool has no drawings. Returns true when the pool contains no line fills. A line fill created with `get` counts even before it is drawn with `set`. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.isFull {#entities-linefillpool-isfull} ```flowscope entities.lineFillPool.isFull: bool ``` Reports whether the line fill pool is at capacity. Returns true when the pool has reached its `max` count. You can still call `get` with new keys; doing so removes the oldest line fill. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.lineFillPool.len {#entities-linefillpool-len} ```flowscope entities.lineFillPool.len: int ``` Counts the line fills currently in the pool. Returns the number of line fills currently in the pool, including any created with `get` that have not yet been drawn. Removed line fills are not counted. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.linePool.capacity {#entities-linepool-capacity} ```flowscope entities.linePool.capacity: int ``` Returns the maximum number of lines in the pool. Returns the `max` value accepted for the line pool at setup. The capacity does not change during the run. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.linePool.isEmpty {#entities-linepool-isempty} ```flowscope entities.linePool.isEmpty: bool ``` Reports whether the line pool has no drawings. Returns true when the pool contains no lines. A line created with `get` counts even before it is drawn with `set`. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.linePool.isFull {#entities-linepool-isfull} ```flowscope entities.linePool.isFull: bool ``` Reports whether the line pool is at capacity. Returns true when the pool has reached its `max` count. You can still call `get` with new keys; doing so removes the oldest line. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.linePool.len {#entities-linepool-len} ```flowscope entities.linePool.len: int ``` Counts the lines currently in the pool. Returns the number of lines currently in the pool, including any created with `get` that have not yet been drawn. Removed lines are not counted. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.markerPool.capacity {#entities-markerpool-capacity} ```flowscope entities.markerPool.capacity: int ``` Returns the maximum number of markers in the pool. Returns the `max` value accepted for the marker pool at setup. The capacity does not change during the run. **Returns** `int` **Available in** Handlers and functions called from handlers. ### entities.markerPool.isEmpty {#entities-markerpool-isempty} ```flowscope entities.markerPool.isEmpty: bool ``` Reports whether the marker pool has no drawings. Returns true when the pool contains no markers. A marker created with `get` counts even before it is drawn with `set`. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.markerPool.isFull {#entities-markerpool-isfull} ```flowscope entities.markerPool.isFull: bool ``` Reports whether the marker pool is at capacity. Returns true when the pool has reached its `max` count. You can still call `get` with new keys; doing so removes the oldest marker. **Returns** `bool` **Available in** Handlers and functions called from handlers. ### entities.markerPool.len {#entities-markerpool-len} ```flowscope entities.markerPool.len: int ``` Counts the markers currently in the pool. Returns the number of markers currently in the pool, including any created with `get` that have not yet been drawn. Removed markers are not counted. **Returns** `int` **Available in** Handlers and functions called from handlers. ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `anchor.bottom` | `Anchor` | `11` | Pins y to logical pixels up from the pane's bottom edge; x stays in chart space. | | `anchor.bottomCenter` | `Anchor` | `8` | Offsets from the center of the pane's bottom edge. | | `anchor.bottomLeft` | `Anchor` | `7` | Offsets inward from the pane's bottom-left corner. | | `anchor.bottomRight` | `Anchor` | `9` | Offsets inward from the pane's bottom-right corner. | | `anchor.center` | `Anchor` | `5` | Offsets from the center of the pane. | | `anchor.left` | `Anchor` | `12` | Pins x to logical pixels right of the pane's left edge; y stays in chart space. | | `anchor.middleLeft` | `Anchor` | `4` | Offsets from the center of the pane's left edge. | | `anchor.middleRight` | `Anchor` | `6` | Offsets from the center of the pane's right edge. | | `anchor.right` | `Anchor` | `13` | Pins x to logical pixels left of the pane's right edge; y stays in chart space. | | `anchor.top` | `Anchor` | `10` | Pins y to logical pixels down from the pane's top edge; x stays in chart space. | | `anchor.topCenter` | `Anchor` | `2` | Offsets from the center of the pane's top edge. | | `anchor.topLeft` | `Anchor` | `1` | Offsets inward from the pane's top-left corner. | | `anchor.topRight` | `Anchor` | `3` | Offsets inward from the pane's top-right corner. | | `font.app` | `FontFamily` | `0` | Uses the normal chart application font. | | `font.mono` | `FontFamily` | `1` | Uses the application monospace font. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/entities --- # alert reference > Declared alerts and their triggers. Declare an alert at top level with `alert name = alert(title: ...)` and fire it from a handler with `name.trigger()`. Alerts fire on live data only. ## Functions ### alert {#alert} ```flowscope alert(title: string, description: string = "", onClose: bool = false, payload: Type = null) -> Alert ``` Declares an alert that the script can trigger. Use this directly in an `alert` declaration. Keep the declaration's name stable; it identifies the alert. - `title` is the required display name. `description` adds optional help text. Both must be string literals. - `onClose` defaults to false, allowing the alert to fire on the first trigger during a bar. Set it to true to fire only when a bar is confirmed. - `payload` optionally names a declared record type. Its fields can be string, int, float, bool, or nullable versions of those types. Omit it or use null for an alert with no payload. Alerts fire only on live data, at most once per bar for each declaration. | Parameter | Type | |---|---| | `title` | `string` | | `description` | `string` | | `onClose` | `bool` | | `payload` | `AlertPayload?` | **Returns** `Alert` **Available in** Setup only: declarations, `setup` and `state` initialisers. ## Methods ### alert.trigger {#alert-trigger} ```flowscope alert.trigger(payload: P = null) -> void ``` Triggers an alert on live data. Call this on an alert you declared, or an alias of that alert, from a handler or a function called by a handler. For an alert with a payload, pass one record of its declared type. For an alert without a payload, pass no argument. The call returns no value. Each alert can fire at most once per bar. Historical replay evaluates the arguments but does not send an alert. With `onClose: true`, triggers during a forming bar do not send an alert either. | Parameter | Type | |---|---| | `payload` | `AlertPayload?` | **Available in** Handlers and functions called from handlers. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/alert --- # log reference > Messages to the script log. `log.*` writes messages with the time of the bar being processed. ## Functions ### log.debug {#log-debug} ```flowscope log.debug(message: string) -> void ``` Writes a debug-level message to the script log. `message` is the text to write and cannot be null. Use this for messages that help you develop or debug a script. Your data plan limits the number of log messages. Messages beyond the limit are dropped, and the number dropped is reported. | Parameter | Type | |---|---| | `message` | `string` | **Available in** Handlers and functions called from handlers. ### log.error {#log-error} ```flowscope log.error(message: string) -> void ``` Writes an error-level message to the script log. `message` is the non-null text to write at the highest log severity. Writing an error message does not stop the script. Log volume is capped by your data plan. Messages beyond the cap are dropped, and the number dropped is reported. | Parameter | Type | |---|---| | `message` | `string` | **Available in** Handlers and functions called from handlers. ### log.info {#log-info} ```flowscope log.info(message: string) -> void ``` Writes an info-level message to the script log. `message` is the text to write and cannot be null. Use this for normal script events you want to record. Your data plan limits the number of log messages. Messages beyond the limit are dropped, and the number dropped is reported. | Parameter | Type | |---|---| | `message` | `string` | **Available in** Handlers and functions called from handlers. ### log.warn {#log-warn} ```flowscope log.warn(message: string) -> void ``` Writes a warning-level message to the script log. `message` is the text to write and cannot be null. Use this for unexpected conditions that the script can handle. Your data plan limits the number of log messages. Messages beyond the limit are dropped, and the number dropped is reported. | Parameter | Type | |---|---| | `message` | `string` | **Available in** Handlers and functions called from handlers. Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/log --- # strategy reference > Strategy declaration, orders, position and results. `strategy(initialCapital: 100000.0, defaultQtyValue: 1.0)` after the header turns a script into a strategy, backtested by a broker emulator that follows TradingView. Place orders from handlers with `strategy.entry`, `strategy.exit`, `strategy.close` and friends; orders fill from the next bar's open, or at the signal bar's close with `processOrdersOnClose: true`. Results appear in the Strategy Tester and as marks on the chart. ## Functions ### strategy.entry {#strategy-entry} ```flowscope strategy.entry(id: string, direction: Direction, qty: float? = null, limit:, stop:, ocaName:, ocaType:, comment:, alertMessage:) ``` Enters a position; an entry in the other direction reverses it. A market order unless `limit` or `stop` is given. `qty` defaults to the declaration's `defaultQtyType` and `defaultQtyValue`. Entries in the same direction are limited by `pyramiding`. Placing an entry with the id of a pending order replaces it. | Parameter | Type | Default | |---|---|---| | `id` | `string` | — | | `direction` | `Direction` | — | | `qty` | `float?` | `null` | | `limit` | `float?` | `null` | | `stop` | `float?` | `null` | | `ocaName` | `string?` | `null` | | `ocaType` | `OcaType?` | `null` | | `comment` | `string?` | `null` | | `alertMessage` | `string?` | `null` | ### strategy.order {#strategy-order} ```flowscope strategy.order(id: string, direction: Direction, qty: float? = null, limit:, stop:, ocaName:, ocaType:, comment:, alertMessage:) ``` Places a raw order: buys or sells `qty`, ignoring pyramiding and without reversing. Unlike `strategy.entry`, an order in the opposite direction reduces the position by its size. | Parameter | Type | Default | |---|---|---| | `id` | `string` | — | | `direction` | `Direction` | — | | `qty` | `float?` | `null` | | `limit` | `float?` | `null` | | `stop` | `float?` | `null` | | `ocaName` | `string?` | `null` | | `ocaType` | `OcaType?` | `null` | | `comment` | `string?` | `null` | | `alertMessage` | `string?` | `null` | ### strategy.exit {#strategy-exit} ```flowscope strategy.exit(id: string, fromEntry: string? = null, qty:, qtyPercent:, profit:, limit:, loss:, stop:, trailPrice:, trailPoints:, trailOffset:, ocaName:, comment:, commentProfit:, commentLoss:, commentTrailing:, alertMessage:) ``` Places take-profit, stop-loss and trailing-stop orders for an entry. `profit` and `loss` are distances in ticks from the entry price, `limit` and `stop` absolute prices; the trailing stop activates at `trailPrice` (or `trailPoints` ticks in profit) and follows the price `trailOffset` ticks behind. Without `fromEntry` the exit applies to every open entry. | Parameter | Type | Default | |---|---|---| | `id` | `string` | — | | `fromEntry` | `string?` | `null` | | `qty` | `float?` | `null` | | `qtyPercent` | `float?` | `null` | | `profit` | `float?` | `null` | | `limit` | `float?` | `null` | | `loss` | `float?` | `null` | | `stop` | `float?` | `null` | | `trailPrice` | `float?` | `null` | | `trailPoints` | `float?` | `null` | | `trailOffset` | `float?` | `null` | | `ocaName` | `string?` | `null` | | `comment` | `string?` | `null` | | `commentProfit` | `string?` | `null` | | `commentLoss` | `string?` | `null` | | `commentTrailing` | `string?` | `null` | | `alertMessage` | `string?` | `null` | ### strategy.close {#strategy-close} ```flowscope strategy.close(id: string, qty:, qtyPercent:, comment:, alertMessage:, immediately: bool = false) ``` Closes the trades of entry `id` with a market order. Closes all of them unless `qty` or `qtyPercent` is given. With `immediately: true` the order fills at the current close instead of the next open. | Parameter | Type | Default | |---|---|---| | `id` | `string` | — | | `qty` | `float?` | `null` | | `qtyPercent` | `float?` | `null` | | `comment` | `string?` | `null` | | `alertMessage` | `string?` | `null` | | `immediately` | `bool` | `false` | ### strategy.closeAll {#strategy-closeall} ```flowscope strategy.closeAll(comment:, alertMessage:, immediately: bool = false) ``` Closes the whole position with a market order. | Parameter | Type | Default | |---|---|---| | `comment` | `string?` | `null` | | `alertMessage` | `string?` | `null` | | `immediately` | `bool` | `false` | ### strategy.cancel {#strategy-cancel} ```flowscope strategy.cancel(id: string) ``` Cancels the pending orders with this id. | Parameter | Type | |---|---| | `id` | `string` | ### strategy.cancelAll {#strategy-cancelall} ```flowscope strategy.cancelAll() ``` Cancels every pending order. ### strategy.openTrade {#strategy-opentrade} ```flowscope strategy.openTrade(index: int) -> StrategyTrade? ``` The open trade at `index`, oldest first, or null. | Parameter | Type | |---|---| | `index` | `int` | **Returns** `StrategyTrade?` ### strategy.closedTrade {#strategy-closedtrade} ```flowscope strategy.closedTrade(index: int) -> StrategyTrade? ``` The closed trade at `index`, oldest first, or null. | Parameter | Type | |---|---| | `index` | `int` | **Returns** `StrategyTrade?` ## Properties ### strategy.positionSize {#strategy-positionsize} ```flowscope strategy.positionSize: float ``` Position size: positive long, negative short, 0 flat. **Returns** `float` ### strategy.positionAvgPrice {#strategy-positionavgprice} ```flowscope strategy.positionAvgPrice: float? ``` Average entry price of the open position, or null when flat. **Returns** `float?` ### strategy.positionEntryName {#strategy-positionentryname} ```flowscope strategy.positionEntryName: string? ``` Id of the entry that opened the position, or null when flat. **Returns** `string?` ### strategy.initialCapital {#strategy-initialcapital} ```flowscope strategy.initialCapital: float ``` The declared initial capital. **Returns** `float` ### strategy.equity {#strategy-equity} ```flowscope strategy.equity: float ``` Initial capital plus net profit plus open profit. **Returns** `float` ### strategy.netProfit {#strategy-netprofit} ```flowscope strategy.netProfit: float ``` Sum of the closed trades' profits after commission and funding. **Returns** `float` ### strategy.grossProfit {#strategy-grossprofit} ```flowscope strategy.grossProfit: float ``` Sum of the winning trades' profits. **Returns** `float` ### strategy.grossLoss {#strategy-grossloss} ```flowscope strategy.grossLoss: float ``` Sum of the losing trades' losses, as a positive number. **Returns** `float` ### strategy.openProfit {#strategy-openprofit} ```flowscope strategy.openProfit: float ``` Unrealised profit of the open trades. **Returns** `float` ### strategy.openTrades {#strategy-opentrades} ```flowscope strategy.openTrades: int ``` Number of open trades. **Returns** `int` ### strategy.closedTrades {#strategy-closedtrades} ```flowscope strategy.closedTrades: int ``` Number of closed trades. **Returns** `int` ### strategy.winTrades {#strategy-wintrades} ```flowscope strategy.winTrades: int ``` Number of closed trades with a profit. **Returns** `int` ### strategy.lossTrades {#strategy-losstrades} ```flowscope strategy.lossTrades: int ``` Number of closed trades with a loss. **Returns** `int` ### strategy.evenTrades {#strategy-eventrades} ```flowscope strategy.evenTrades: int ``` Number of closed trades at break-even. **Returns** `int` ### strategy.maxDrawdown {#strategy-maxdrawdown} ```flowscope strategy.maxDrawdown: float ``` Largest equity drawdown so far, in account currency. **Returns** `float` ### strategy.maxRunup {#strategy-maxrunup} ```flowscope strategy.maxRunup: float ``` Largest equity run-up so far, in account currency. **Returns** `float` ### strategy.maxContractsHeldAll {#strategy-maxcontractsheldall} ```flowscope strategy.maxContractsHeldAll: float ``` Largest position held so far, either side. **Returns** `float` ### strategy.maxContractsHeldLong {#strategy-maxcontractsheldlong} ```flowscope strategy.maxContractsHeldLong: float ``` Largest long position held so far. **Returns** `float` ### strategy.maxContractsHeldShort {#strategy-maxcontractsheldshort} ```flowscope strategy.maxContractsHeldShort: float ``` Largest short position held so far. **Returns** `float` ### strategy.marginLiquidationPrice {#strategy-marginliquidationprice} ```flowscope strategy.marginLiquidationPrice: float? ``` Price at which a margin call would liquidate the position, or null. **Returns** `float?` ## Records ### StrategyTrade {#strategytrade} ```flowscope type StrategyTrade ``` A trade returned by `strategy.openTrade` and `strategy.closedTrade`. | Field | Type | History | Description | |---|---|---|---| | `entryId` | `string` | — | | | `entryPrice` | `float` | — | | | `entryTime` | `time` | — | | | `entryBarIndex` | `int` | — | | | `entryComment` | `string?` | — | | | `size` | `float` | — | | | `exitId` | `string?` | — | | | `exitPrice` | `float?` | — | | | `exitTime` | `time?` | — | | | `exitBarIndex` | `int?` | — | | | `exitComment` | `string?` | — | | | `profit` | `float` | — | | | `profitPercent` | `float` | — | | | `commission` | `float` | — | | | `maxRunup` | `float` | — | | | `maxDrawdown` | `float` | — | | ## Constants | Constant | Type | Value | Description | |---|---|---|---| | `strategy.long` | `Direction` | `1` | Long direction for `strategy.entry` and `strategy.order`. | | `strategy.short` | `Direction` | `2` | Short direction for `strategy.entry` and `strategy.order`. | | `strategy.fixed` | `QtyType` | `0` | `defaultQtyType`: `defaultQtyValue` contracts per order. | | `strategy.cash` | `QtyType` | `1` | `defaultQtyType`: as many contracts as `defaultQtyValue` of account currency buys. | | `strategy.percentOfEquity` | `QtyType` | `2` | `defaultQtyType`: `defaultQtyValue` percent of current equity per order. | | `strategy.commission.percent` | `CommissionType` | `0` | `commissionType`: percent of the order value. | | `strategy.commission.cashPerContract` | `CommissionType` | `1` | `commissionType`: money per contract traded. | | `strategy.commission.cashPerOrder` | `CommissionType` | `2` | `commissionType`: money per filled order. | | `strategy.oca.none` | `OcaType` | `0` | No one-cancels-all behaviour. | | `strategy.oca.cancel` | `OcaType` | `1` | A fill cancels the other orders of the OCA group. | | `strategy.oca.reduce` | `OcaType` | `2` | A fill reduces the other orders of the OCA group by the filled size. | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/catalog/strategy --- # Diagnostics > Stable compiler diagnostic codes of Flowscope Script, each with a failing and a corrected example. Every message the compiler reports carries a stable code. The letter names the stage that found the problem; the number never changes meaning, so you can search for it, link to it and silence nothing by accident. Errors stop the script from running; warnings point at code that runs but probably does not do what you meant. ## Parse (P…) Problems found while reading the source text: characters, literals, line structure and syntax forms that Flowscope Script does not have. | Code | Severity | Summary | |---|---|---| | [P001](/docs/scripting/reference/diagnostics/p001) | error | unrecognized token | | [P002](/docs/scripting/reference/diagnostics/p002) | error | unterminated string literal | | [P003](/docs/scripting/reference/diagnostics/p003) | error | semicolon is not supported | | [P005](/docs/scripting/reference/diagnostics/p005) | error | `??` mixed with `&&` or `\|\|` needs parentheses | | [P006](/docs/scripting/reference/diagnostics/p006) | error | `while` is not supported | | [P007](/docs/scripting/reference/diagnostics/p007) | error | template strings are not supported | | [P008](/docs/scripting/reference/diagnostics/p008) | error | `?.` is not supported | | [P009](/docs/scripting/reference/diagnostics/p009) | error | postfix `!` is not supported | | [P011](/docs/scripting/reference/diagnostics/p011) | error | top-level statement is not a declaration | | [P014](/docs/scripting/reference/diagnostics/p014) | error | `break` and `continue` are not statements | | [P016](/docs/scripting/reference/diagnostics/p016) | error | expected token | | [P021](/docs/scripting/reference/diagnostics/p021) | error | invalid color literal | | [P022](/docs/scripting/reference/diagnostics/p022) | error | unknown string escape | | [P023](/docs/scripting/reference/diagnostics/p023) | error | match arm in statement form needs a block | | [P024](/docs/scripting/reference/diagnostics/p024) | error | match expression arm takes an expression, not a block | | [P025](/docs/scripting/reference/diagnostics/p025) | error | keyword used as a name | | [P026](/docs/scripting/reference/diagnostics/p026) | error | ternary broken before `?` or `:` | | [P027](/docs/scripting/reference/diagnostics/p027) | error | second statement on the same line | ## Setup and declarations (S…) Declarations that cannot be evaluated before the first bar, misuse of setup-only constructors, subscription arguments and alert declarations. | Code | Severity | Summary | |---|---|---| | [S001](/docs/scripting/reference/diagnostics/s001) | error | live value used in setup | | [S002](/docs/scripting/reference/diagnostics/s002) | error | built-in cannot run in setup | | [S005](/docs/scripting/reference/diagnostics/s005) | error | collection capacity must be known before the script runs | | [S006](/docs/scripting/reference/diagnostics/s006) | error | duplicate panel name | | [S007](/docs/scripting/reference/diagnostics/s007) | error | constructor does not fit the declaration keyword | | [S008](/docs/scripting/reference/diagnostics/s008) | error | user function called in setup | | [S011](/docs/scripting/reference/diagnostics/s011) | error | setup value reads a later declaration | | [S012](/docs/scripting/reference/diagnostics/s012) | error | empty exchange list in subscription | | [S013](/docs/scripting/reference/diagnostics/s013) | error | color scale needs matching lists with at least two entries | | [S015](/docs/scripting/reference/diagnostics/s015) | error | minimum trade size must be positive and finite | | [S018](/docs/scripting/reference/diagnostics/s018) | error | lambda or collection transform in setup | | [S019](/docs/scripting/reference/diagnostics/s019) | error | alert constructor outside an alert declaration | | [S020](/docs/scripting/reference/diagnostics/s020) | error | invalid alert payload type | | [S022](/docs/scripting/reference/diagnostics/s022) | error | trigger argument does not match the alert payload | | [S026](/docs/scripting/reference/diagnostics/s026) | error | subscription history cannot be negative | | [S027](/docs/scripting/reference/diagnostics/s027) | error | the chart subscription cannot be live only | | [S028](/docs/scripting/reference/diagnostics/s028) | error | leverage bound below 1 | ## Types and names (T…) Name resolution and type checking. T0xx names and declarations, T1xx types and literals, T2xx calls, arguments and returns, T3xx user types and match, T4xx handlers, series, plots and lambdas. | Code | Severity | Summary | |---|---|---| | [T001](/docs/scripting/reference/diagnostics/t001) | error | unknown identifier | | [T002](/docs/scripting/reference/diagnostics/t002) | error | duplicate top-level name | | [T005](/docs/scripting/reference/diagnostics/t005) | error | unknown type name | | [T006](/docs/scripting/reference/diagnostics/t006) | error | local hides a built-in namespace | | [T007](/docs/scripting/reference/diagnostics/t007) | error | name is already bound in this scope | | [T010](/docs/scripting/reference/diagnostics/t010) | error | script header missing or misplaced | | [T011](/docs/scripting/reference/diagnostics/t011) | warning | duplicate display title | | [T013](/docs/scripting/reference/diagnostics/t013) | warning | unused binding | | [T014](/docs/scripting/reference/diagnostics/t014) | warning | unused subscription | | [T016](/docs/scripting/reference/diagnostics/t016) | error | duplicate input key | | [T101](/docs/scripting/reference/diagnostics/t101) | error | type mismatch | | [T102](/docs/scripting/reference/diagnostics/t102) | error | operator not defined for these operand types | | [T103](/docs/scripting/reference/diagnostics/t103) | error | float needs an explicit conversion to int | | [T106](/docs/scripting/reference/diagnostics/t106) | error | invalid map key type | | [T109](/docs/scripting/reference/diagnostics/t109) | error | type is not indexable | | [T111](/docs/scripting/reference/diagnostics/t111) | error | invalid timeframe literal | | [T112](/docs/scripting/reference/diagnostics/t112) | error | zero-length timeframe | | [T113](/docs/scripting/reference/diagnostics/t113) | error | integer literal out of range | | [T114](/docs/scripting/reference/diagnostics/t114) | error | invalid session literal | | [T201](/docs/scripting/reference/diagnostics/t201) | error | unknown named argument | | [T202](/docs/scripting/reference/diagnostics/t202) | error | positional argument after a named one | | [T203](/docs/scripting/reference/diagnostics/t203) | error | argument supplied more than once | | [T204](/docs/scripting/reference/diagnostics/t204) | error | missing argument | | [T205](/docs/scripting/reference/diagnostics/t205) | error | too many positional values | | [T206](/docs/scripting/reference/diagnostics/t206) | error | parameter default is not a literal | | [T207](/docs/scripting/reference/diagnostics/t207) | error | required parameter after an optional one | | [T208](/docs/scripting/reference/diagnostics/t208) | error | null default for a non-nullable parameter | | [T211](/docs/scripting/reference/diagnostics/t211) | error | wrong number of return values | | [T212](/docs/scripting/reference/diagnostics/t212) | error | not every path returns a value | | [T214](/docs/scripting/reference/diagnostics/t214) | error | destructuring does not match the returned values | | [T215](/docs/scripting/reference/diagnostics/t215) | error | no overload matches these arguments | | [T216](/docs/scripting/reference/diagnostics/t216) | error | value cannot be reassigned | | [T217](/docs/scripting/reference/diagnostics/t217) | error | invalid assignment target | | [T301](/docs/scripting/reference/diagnostics/t301) | error | type field has no type annotation | | [T302](/docs/scripting/reference/diagnostics/t302) | error | type contains itself | | [T303](/docs/scripting/reference/diagnostics/t303) | error | type name is not UpperCamelCase | | [T304](/docs/scripting/reference/diagnostics/t304) | error | match does not cover every case | | [T305](/docs/scripting/reference/diagnostics/t305) | warning | null arm on a value that cannot be null | | [T306](/docs/scripting/reference/diagnostics/t306) | error | record literal has wrong fields | | [T307](/docs/scripting/reference/diagnostics/t307) | error | duplicate field in a type | | [T401](/docs/scripting/reference/diagnostics/t401) | error | function calls itself | | [T402](/docs/scripting/reference/diagnostics/t402) | error | stateful calculation inside a loop | | [T403](/docs/scripting/reference/diagnostics/t403) | error | indicator inputs come from different timeframes | | [T405](/docs/scripting/reference/diagnostics/t405) | error | event not available for this subscription | | [T406](/docs/scripting/reference/diagnostics/t406) | error | duplicate handler | | [T407](/docs/scripting/reference/diagnostics/t407) | error | handler on a snapshot source | | [T408](/docs/scripting/reference/diagnostics/t408) | error | indicator on a calendar timeframe | | [T413](/docs/scripting/reference/diagnostics/t413) | error | indicator length must be known before the script runs | | [T414](/docs/scripting/reference/diagnostics/t414) | error | indicator length out of range | | [T415](/docs/scripting/reference/diagnostics/t415) | error | plot method does not match the panel | | [T416](/docs/scripting/reference/diagnostics/t416) | error | invalid str.format template | | [T420](/docs/scripting/reference/diagnostics/t420) | error | trade fields are read-only | | [T422](/docs/scripting/reference/diagnostics/t422) | error | book or profile view kept beyond the current call | | [T425](/docs/scripting/reference/diagnostics/t425) | error | fill has both a solid color and a gradient | | [T426](/docs/scripting/reference/diagnostics/t426) | error | lambda in an invalid position or with a block body | | [T427](/docs/scripting/reference/diagnostics/t427) | error | invalid lambda parameter list | | [T428](/docs/scripting/reference/diagnostics/t428) | error | stateful call, randomness or drawing inside a lambda | | [T430](/docs/scripting/reference/diagnostics/t430) | error | collection transform must be its own statement | | [T442](/docs/scripting/reference/diagnostics/t442) | error | record literal without a type name | | [T449](/docs/scripting/reference/diagnostics/t449) | warning | isLast read in a close handler | ## Null safety (N…) Places where a value that may be missing is used as if it were always present. | Code | Severity | Summary | |---|---|---| | [N001](/docs/scripting/reference/diagnostics/n001) | error | value may be null here | | [N002](/docs/scripting/reference/diagnostics/n002) | error | null check on a field does not narrow it | | [N003](/docs/scripting/reference/diagnostics/n003) | error | null needs an explicit type | ## Collections (C…) Arrays, rolling buffers and maps: read-only setup data, element types, capacities and subscription series. | Code | Severity | Summary | |---|---|---| | [C001](/docs/scripting/reference/diagnostics/c001) | error | setup collection is read-only | | [C003](/docs/scripting/reference/diagnostics/c003) | error | collection cannot contain another collection | | [C004](/docs/scripting/reference/diagnostics/c004) | error | key is missing from a setup map | | [C005](/docs/scripting/reference/diagnostics/c005) | error | empty array needs a capacity | | [C006](/docs/scripting/reference/diagnostics/c006) | error | subscription series is read-only | ## Limits (L…) Fixed resource limits per script: stateful calls, subscriptions, capacities, inputs, plots and alerts. | Code | Severity | Summary | |---|---|---| | [L001](/docs/scripting/reference/diagnostics/l001) | error | too many stateful call sites | | [L006](/docs/scripting/reference/diagnostics/l006) | error | too many subscriptions | | [L007](/docs/scripting/reference/diagnostics/l007) | error | capacity exceeds the supported limit | | [L008](/docs/scripting/reference/diagnostics/l008) | error | negative capacity | | [L010](/docs/scripting/reference/diagnostics/l010) | error | too many inputs | | [L011](/docs/scripting/reference/diagnostics/l011) | error | too many plots | | [L013](/docs/scripting/reference/diagnostics/l013) | error | too many alerts | | [L015](/docs/scripting/reference/diagnostics/l015) | error | expressions or blocks nest too deeply | ## Backtesting and strategies (B…) The strategy declaration, order functions and broker emulator settings used by the Strategy Tester. | Code | Severity | Summary | |---|---|---| | [B001](/docs/scripting/reference/diagnostics/b001) | error | strategy order outside a handler | | [B002](/docs/scripting/reference/diagnostics/b002) | error | strategy function without a strategy declaration | | [B003](/docs/scripting/reference/diagnostics/b003) | error | more than one strategy declaration | | [B004](/docs/scripting/reference/diagnostics/b004) | error | strategy property set twice | | [B005](/docs/scripting/reference/diagnostics/b005) | warning | exit refers to an unknown entry id | | [B006](/docs/scripting/reference/diagnostics/b006) | error | pyramiding is negative | | [B007](/docs/scripting/reference/diagnostics/b007) | error | quantity is not positive | | [B008](/docs/scripting/reference/diagnostics/b008) | error | commission or slippage is negative | | [B009](/docs/scripting/reference/diagnostics/b009) | error | exit target given in ticks and as a price | | [B010](/docs/scripting/reference/diagnostics/b010) | error | trailOffset without trailPoints | | [B011](/docs/scripting/reference/diagnostics/b011) | error | leverage out of range | | [B012](/docs/scripting/reference/diagnostics/b012) | warning | processOrdersOnClose combined with the bar magnifier | | [B013](/docs/scripting/reference/diagnostics/b013) | error | strategy position read in setup | | [B014](/docs/scripting/reference/diagnostics/b014) | error | initial capital is not positive | ## Internal (I…) Failures of the compiler itself rather than of the script. | Code | Severity | Summary | |---|---|---| | [I000](/docs/scripting/reference/diagnostics/i000) | error | internal compiler error | Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics --- # P001 > unrecognized token **Severity** Error The lexer met a character that does not start any Flowscope Script token. The most common source is text pasted from a web page or a chat window: typographic minus signs (`−`), curly quotes (`“ ”`), non-breaking spaces or other look-alike Unicode characters. Replace the character with its plain ASCII counterpart. Operators are `+ - * /`, strings use straight double quotes, and identifiers consist of letters, digits and `_`. ## Failing example ```flowscope script "Bar range" data chart = subscribe(data.ohlcv) plot range = plot.histogram(title: "High minus low") on chart.close { range.plot(chart.high − chart.low) } ``` ## Fixed example ```flowscope script "Bar range" data chart = subscribe(data.ohlcv) plot range = plot.histogram(title: "High minus low") on chart.close { range.plot(chart.high - chart.low) } ``` ## Related - [P016](/docs/scripting/reference/diagnostics/p016) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p001 --- # P002 > unterminated string literal **Severity** Error A string literal starts with `"` but the line ends before the closing quote. Strings cannot span several lines; a line break inside a string has to be written as the escape `\n`. Add the missing closing quote. If the text itself contains a double quote, escape it as `\"`. ## Failing example ```flowscope script "Funding monitor" data funding = subscribe(data.funding) pane fp = pane(title: "Funding", height: 0.2) plot rate = plot.histogram(title: "Funding rate, on: fp) on funding.close { rate.plot(funding.rate) } ``` ## Fixed example ```flowscope script "Funding monitor" data funding = subscribe(data.funding) pane fp = pane(title: "Funding", height: 0.2) plot rate = plot.histogram(title: "Funding rate", on: fp) on funding.close { rate.plot(funding.rate) } ``` ## Related - [P022](/docs/scripting/reference/diagnostics/p022) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p002 --- # P003 > semicolon is not supported **Severity** Error Flowscope Script ends every statement at the end of its line, so a trailing `;` has no meaning and is rejected rather than silently ignored. Delete the semicolon. If you meant to put two statements on one line, put each on its own line instead. ## Failing example ```flowscope script "Open interest" data oi = subscribe(data.oi) pane p = pane(title: "OI", height: 0.25) plot line = plot.line(title: "Open interest", on: p) on oi.close { line.plot(oi.close); } ``` ## Fixed example ```flowscope script "Open interest" data oi = subscribe(data.oi) pane p = pane(title: "OI", height: 0.25) plot line = plot.line(title: "Open interest", on: p) on oi.close { line.plot(oi.close) } ``` ## Related - [P027](/docs/scripting/reference/diagnostics/p027) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p003 --- # P005 > `??` mixed with `&&` or `||` needs parentheses **Severity** Error An expression combines the null-coalescing operator `??` with `&&` or `||` without parentheses. Whatever precedence the parser picks, a reader can easily assume the other one, so the combination must be grouped explicitly. Wrap the part you mean in parentheses, typically the coalescing part: `(armed ?? false) && chart.isRealtime`. ## Failing example ```flowscope script "Armed breakout" data chart = subscribe(data.ohlcv) state armed: bool? = null alert hit = alert(title: "Breakout while armed") on chart.close { if armed ?? false && chart.isRealtime { hit.trigger() } armed = chart.close > chart.high[1] } ``` ## Fixed example ```flowscope script "Armed breakout" data chart = subscribe(data.ohlcv) state armed: bool? = null alert hit = alert(title: "Breakout while armed") on chart.close { if (armed ?? false) && chart.isRealtime { hit.trigger() } armed = chart.close > chart.high[1] } ``` ## Related - [N001](/docs/scripting/reference/diagnostics/n001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p005 --- # P006 > `while` is not supported **Severity** Error Flowscope Script only has bounded loops, so that every handler call is guaranteed to finish within the step budget. A `while` loop could run an unknown number of times and is therefore not part of the language. Rewrite the loop as `for i in 0..n { }` over a known range, or as `for x in xs { }` over a collection. ## Failing example ```flowscope script "Five-bar volume" data chart = subscribe(data.ohlcv) plot vol5 = plot.histogram(title: "Volume, last 5 bars") state acc: float = 0.0 state i: int = 0 on chart.close { acc = 0.0 i = 0 while i < 5 { acc += chart.volume[i] ?? 0.0 i += 1 } vol5.plot(acc) } ``` ## Fixed example ```flowscope script "Five-bar volume" data chart = subscribe(data.ohlcv) plot vol5 = plot.histogram(title: "Volume, last 5 bars") state acc: float = 0.0 on chart.close { acc = 0.0 for i in 0..5 { acc += chart.volume[i] ?? 0.0 } vol5.plot(acc) } ``` ## Related - [P014](/docs/scripting/reference/diagnostics/p014) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p006 --- # P007 > template strings are not supported **Severity** Error A string contains `${...}`. Flowscope Script strings are plain text with no interpolation, so the placeholder would be printed literally; the compiler rejects it instead. Build the text with `str.format`, which also lets you control decimals: `str.format("Funding {0:.4}%", rate)`. ## Failing example ```flowscope script "Funding log" data funding = subscribe(data.funding) on funding.close { let rate = funding.rate ?? 0.0 log.info("Funding ${rate}%") } ``` ## Fixed example ```flowscope script "Funding log" data funding = subscribe(data.funding) on funding.close { let rate = funding.rate ?? 0.0 log.info(str.format("Funding {0:.4}%", rate)) } ``` ## Related - [T416](/docs/scripting/reference/diagnostics/t416) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p007 --- # P008 > `?.` is not supported **Severity** Error Optional chaining (`a?.b`) does not exist in Flowscope Script. Nullable values are handled explicitly so that every place that can produce a gap is visible in the code. Store the nullable value in a local, check it with `if x != null { }`, and read the member inside the block. For a simple default, `??` on the field of a checked local works as well. ## Failing example ```flowscope script "Best bid" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) plot bid = plot.line(title: "Best bid", color: color.bull) on chart.update { bid.plot(book.level(side.bid, 0)?.price) } ``` ## Fixed example ```flowscope script "Best bid" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) plot bid = plot.line(title: "Best bid", color: color.bull) on chart.update { let top = book.level(side.bid, 0) if top != null { bid.plot(top.price) } } ``` ## Related - [P009](/docs/scripting/reference/diagnostics/p009) - [N001](/docs/scripting/reference/diagnostics/n001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p008 --- # P009 > postfix `!` is not supported **Severity** Error A postfix `!` (a "non-null assertion") is not part of the language. Flowscope Script never lets you claim that a value is present without checking it, because a missing bar or an empty book is a normal situation in market data. Check the value with `if x != null` first, or provide a fallback with `x ?? fallback`. ## Failing example ```flowscope script "Body size" data chart = subscribe(data.ohlcv) plot body = plot.histogram(title: "Body") on chart.close { let c = chart.close! let o = chart.open! body.plot(math.abs(c - o)) } ``` ## Fixed example ```flowscope script "Body size" data chart = subscribe(data.ohlcv) plot body = plot.histogram(title: "Body") on chart.close { let c = chart.close let o = chart.open if c != null && o != null { body.plot(math.abs(c - o)) } } ``` ## Related - [P008](/docs/scripting/reference/diagnostics/p008) - [N001](/docs/scripting/reference/diagnostics/n001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p009 --- # P011 > top-level statement is not a declaration **Severity** Error At file level only declarations are allowed: `input`, `data`, `state`, `setup`, `plot`, `fill`, `pane`, `window`, `alert`, `strategy`, `type`, `fn` and `on`. An executable statement such as a call, an assignment or an `if` was found outside any handler or function. Move the statement into a handler. Code that should run only once can be placed in a handler and guarded with `chart.isFirst`. ## Failing example ```flowscope script "Startup note" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") log.info("script loaded") on chart.close { c.plot(chart.close) } ``` ## Fixed example ```flowscope script "Startup note" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { if chart.isFirst { log.info("script loaded") } c.plot(chart.close) } ``` ## Related - [P016](/docs/scripting/reference/diagnostics/p016) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p011 --- # P014 > `break` and `continue` are not statements **Severity** Error Loops in Flowscope Script always run over their full range or collection; there is no way to jump out early or skip ahead. `break` and `continue` are therefore not accepted. Express the skip as a condition around the loop body. To stop processing after a match, keep a flag in `state` and test it at the top of the body. ## Failing example ```flowscope script "Recent buy volume" data chart = subscribe(data.ohlcv) plot buys = plot.histogram(title: "Buy volume, 10 bars", color: color.bull) state acc: float = 0.0 on chart.close { acc = 0.0 for i in 0..10 { let v = chart.buyVolume[i] if v == null { continue } acc += v } buys.plot(acc) } ``` ## Fixed example ```flowscope script "Recent buy volume" data chart = subscribe(data.ohlcv) plot buys = plot.histogram(title: "Buy volume, 10 bars", color: color.bull) state acc: float = 0.0 on chart.close { acc = 0.0 for i in 0..10 { let v = chart.buyVolume[i] if v != null { acc += v } } buys.plot(acc) } ``` ## Related - [P006](/docs/scripting/reference/diagnostics/p006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p014 --- # P016 > expected token **Severity** Error The parser needed a specific token (a closing bracket, a `{`, a `:` or an expression) and found something else, often the end of the line. The message names the token it expected. Insert the missing token at the position shown. When several P016 errors appear in a row, fix the first one; the rest are frequently follow-on errors. ## Failing example ```flowscope script "Session CVD" data cvd = subscribe(data.cvd, bucket: "session") pane p = pane(title: "CVD", height: 0.25) plot line = plot.line(title: "Session CVD", on: p) on cvd.close line.plot(cvd.close) } ``` ## Fixed example ```flowscope script "Session CVD" data cvd = subscribe(data.cvd, bucket: "session") pane p = pane(title: "CVD", height: 0.25) plot line = plot.line(title: "Session CVD", on: p) on cvd.close { line.plot(cvd.close) } ``` ## Related - [P001](/docs/scripting/reference/diagnostics/p001) - [P011](/docs/scripting/reference/diagnostics/p011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p016 --- # P021 > invalid color literal **Severity** Error A color literal must have exactly six (`#RRGGBB`) or eight (`#RRGGBBAA`) hexadecimal digits after `#`. Three-digit shorthands such as `#2AB` and non-hex characters are rejected. Write all six digits (`#22AABB`), add two more for transparency (`#22AABB80`), or use a named color such as `color.bull`. ## Failing example ```flowscope script "Delta bars" data vd = subscribe(data.vd) pane p = pane(title: "Delta", height: 0.25) plot delta = plot.histogram(title: "Delta", on: p) on vd.close { let d = vd.close ?? 0.0 delta.plot(d, color: d >= 0.0 ? #2AB : #F23645) } ``` ## Fixed example ```flowscope script "Delta bars" data vd = subscribe(data.vd) pane p = pane(title: "Delta", height: 0.25) plot delta = plot.histogram(title: "Delta", on: p) on vd.close { let d = vd.close ?? 0.0 delta.plot(d, color: d >= 0.0 ? #22AABB : #F23645) } ``` ## Related - [P001](/docs/scripting/reference/diagnostics/p001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p021 --- # P022 > unknown string escape **Severity** Error Inside a string a backslash starts an escape sequence. Only `\n` (new line), `\t` (tab), `\"` (double quote) and `\\` (backslash) are recognised; any other letter after a backslash is reported. If you wanted a literal backslash, double it. Usually it is simpler to rephrase, for example with a slash. ## Failing example ```flowscope script "Liquidation split" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) plot ratio = plot.line(title: "Longs\Shorts") on chart.close { let s = liq.shorts ?? 0.0 ratio.plot(s > 0.0 ? (liq.longs ?? 0.0) / s : null) } ``` ## Fixed example ```flowscope script "Liquidation split" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) plot ratio = plot.line(title: "Longs / Shorts") on chart.close { let s = liq.shorts ?? 0.0 ratio.plot(s > 0.0 ? (liq.longs ?? 0.0) / s : null) } ``` ## Related - [P002](/docs/scripting/reference/diagnostics/p002) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p022 --- # P023 > match arm in statement form needs a block **Severity** Error When `match` is used as a statement, each arm runs code rather than producing a value, so its body has to be a block in braces. A bare call after `=>` is only allowed in the expression form of `match`. Wrap each arm body in `{ }`. An arm that should do nothing is written `_ => { }`. ## Failing example ```flowscope script "Funding regime" data funding = subscribe(data.funding) pane p = pane(title: "Funding", height: 0.2) plot hot = plot.histogram(title: "Elevated funding", color: color.bear, on: p) on funding.close { let r = funding.rate ?? 0.0 match r { v if v > 0.0003 => hot.plot(v) _ => { } } } ``` ## Fixed example ```flowscope script "Funding regime" data funding = subscribe(data.funding) pane p = pane(title: "Funding", height: 0.2) plot hot = plot.histogram(title: "Elevated funding", color: color.bear, on: p) on funding.close { let r = funding.rate ?? 0.0 match r { v if v > 0.0003 => { hot.plot(v) } _ => { } } } ``` ## Related - [P024](/docs/scripting/reference/diagnostics/p024) - [T304](/docs/scripting/reference/diagnostics/t304) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p023 --- # P024 > match expression arm takes an expression, not a block **Severity** Error In the expression form (`let x = match ...`), every arm must be a single expression that becomes the result. A block in braces is a sequence of statements and has no value. Remove the braces around the arm value. If an arm needs several steps, compute them before the `match` or turn the match into a statement that assigns a `state` variable. ## Failing example ```flowscope script "RSI zone" data chart = subscribe(data.ohlcv) pane p = pane(title: "RSI", height: 0.25) plot rsi = plot.line(title: "RSI 14", on: p) on chart.close { let r = ta.rsi(chart.close, 14) ?? 50.0 let tint = match r { 0.0..30.0 => { color.bull } 70.0..100.0 => { color.bear } _ => { color.muted } } rsi.plot(r, color: tint) } ``` ## Fixed example ```flowscope script "RSI zone" data chart = subscribe(data.ohlcv) pane p = pane(title: "RSI", height: 0.25) plot rsi = plot.line(title: "RSI 14", on: p) on chart.close { let r = ta.rsi(chart.close, 14) ?? 50.0 let tint = match r { 0.0..30.0 => color.bull 70.0..100.0 => color.bear _ => color.muted } rsi.plot(r, color: tint) } ``` ## Related - [P023](/docs/scripting/reference/diagnostics/p023) - [T304](/docs/scripting/reference/diagnostics/t304) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p024 --- # P025 > keyword used as a name **Severity** Error A reserved word (for example `type`, `match`, `state`, `data`, `on`, `fn`, `for`, `in`, `strategy`) was used as the name of a local, a parameter or a type field. Keywords keep their meaning everywhere, so they cannot name values. Choose a descriptive name instead, such as `kind`, `sideName` or `dataSource`. ## Failing example ```flowscope script "Tape side" data tape = subscribe(data.trades, minNotional: 250_000.0) on tape.trade(tr) { let type = tr.isBuy ? "buy" : "sell" log.info(str.format("{0} {1}", type, str.formatVolume(tr.notional))) } ``` ## Fixed example ```flowscope script "Tape side" data tape = subscribe(data.trades, minNotional: 250_000.0) on tape.trade(tr) { let sideName = tr.isBuy ? "buy" : "sell" log.info(str.format("{0} {1}", sideName, str.formatVolume(tr.notional))) } ``` ## Related - [T006](/docs/scripting/reference/diagnostics/t006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p025 --- # P026 > ternary broken before `?` or `:` **Severity** Error A line ends a statement as soon as the expression on it is complete. If the next line starts with `?` or `:`, the previous line has already been taken as a whole statement and the leftover operator cannot be parsed. Break long ternaries after the operator: end the line with `?` or `:` and continue on the next line. ## Failing example ```flowscope script "Delta tint" data vd = subscribe(data.vd) pane p = pane(title: "Delta", height: 0.25) plot delta = plot.columns(title: "Delta", on: p) on vd.close { let d = vd.close ?? 0.0 let tint = d >= 0.0 ? color.bull : color.bear delta.plot(d, color: tint) } ``` ## Fixed example ```flowscope script "Delta tint" data vd = subscribe(data.vd) pane p = pane(title: "Delta", height: 0.25) plot delta = plot.columns(title: "Delta", on: p) on vd.close { let d = vd.close ?? 0.0 let tint = d >= 0.0 ? color.bull : color.bear delta.plot(d, color: tint) } ``` ## Related - [P027](/docs/scripting/reference/diagnostics/p027) - [P016](/docs/scripting/reference/diagnostics/p016) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p026 --- # P027 > second statement on the same line **Severity** Error Statements are separated by line breaks and by nothing else. After one statement was complete, more code followed on the same line. Move the second statement to its own line. ## Failing example ```flowscope script "Bar counter" data chart = subscribe(data.ohlcv) state bars: int = 0 plot count = plot.line(title: "Bars seen") on chart.close { bars += 1 count.plot(bars) } ``` ## Fixed example ```flowscope script "Bar counter" data chart = subscribe(data.ohlcv) state bars: int = 0 plot count = plot.line(title: "Bars seen") on chart.close { bars += 1 count.plot(bars) } ``` ## Related - [P003](/docs/scripting/reference/diagnostics/p003) - [P026](/docs/scripting/reference/diagnostics/p026) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/p027 --- # S001 > live value used in setup **Severity** Error A `setup` value, a `state` initializer or a declaration argument reads something that only exists while a handler runs: a subscription field, a status flag such as `chart.isLast`, a `state` variable or the strategy position. Setup expressions are evaluated once, before the first bar is delivered, so there is nothing to read yet. Compute the value inside a handler. If it must persist, declare it as `state` with a neutral starting value and assign it in the handler. ## Failing example ```flowscope script "Opening price" data chart = subscribe(data.ohlcv) setup firstOpen = chart.open plot ref = plot.line(title: "First open") on chart.close { ref.plot(firstOpen) } ``` ## Fixed example ```flowscope script "Opening price" data chart = subscribe(data.ohlcv) state firstOpen: float? = null plot ref = plot.line(title: "First open") on chart.close { if firstOpen == null { firstOpen = chart.open } ref.plot(firstOpen) } ``` ## Related - [S002](/docs/scripting/reference/diagnostics/s002) - [S008](/docs/scripting/reference/diagnostics/s008) - [B013](/docs/scripting/reference/diagnostics/b013) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s001 --- # S002 > built-in cannot run in setup **Severity** Error The built-in called in a setup expression is not setup-safe: it reads the wall clock, draws random numbers, writes logs, draws entities or depends on bar data. Only pure built-ins whose result is fixed for the whole run (math, string and color helpers, `context.*`, `tf`, `session`, `color.scale` and so on) may run in setup. Move the call into a handler. The catalog marks each function with the context in which it may be used. ## Failing example ```flowscope script "Uptime" data chart = subscribe(data.ohlcv) setup startedAt = time.now() on chart.update { if chart.isLast { log.info(str.format("running since {0}", time.format(startedAt, "HH:mm"))) } } ``` ## Fixed example ```flowscope script "Uptime" data chart = subscribe(data.ohlcv) state startedAt: time? = null on chart.update { if startedAt == null { startedAt = time.now() } let t = startedAt if chart.isLast && t != null { log.info(str.format("running since {0}", time.format(t, "HH:mm"))) } } ``` ## Related - [S001](/docs/scripting/reference/diagnostics/s001) - [S008](/docs/scripting/reference/diagnostics/s008) - [S018](/docs/scripting/reference/diagnostics/s018) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s002 --- # S005 > collection capacity must be known before the script runs **Severity** Error The capacity of an array, rolling buffer or map is fixed when the script starts so that memory can be reserved once. Here the capacity is computed from something that only becomes known while the script runs, such as a `state` variable. Use a literal, a setup expression built from literals and `context.*`, or an `input` value. ## Failing example ```flowscope script "Rolling closes" data chart = subscribe(data.ohlcv) state size: int = 50 state closes = rolling(size) plot avg = plot.line(title: "Rolling mean") on chart.close { closes.push(chart.close) avg.plot(closes.avg()) } ``` ## Fixed example ```flowscope script "Rolling closes" input size = input.int(50, title: "Window", min: 2, max: 1000) data chart = subscribe(data.ohlcv) state closes = rolling(size) plot avg = plot.line(title: "Rolling mean") on chart.close { closes.push(chart.close) avg.plot(closes.avg()) } ``` ## Related - [T413](/docs/scripting/reference/diagnostics/t413) - [L007](/docs/scripting/reference/diagnostics/l007) - [L008](/docs/scripting/reference/diagnostics/l008) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s005 --- # S006 > duplicate panel name **Severity** Error Two panels inside one `window` declaration use the same field name. Panel names address the panel from plots (`on: w.price`) and keep the window layout stable across restarts, so each must be unique within its window. Rename one of the panels. ## Failing example ```flowscope script "Four-hour window" data h4 = subscribe(data.ohlcv, timeframe: 4h) window w = window(title: "4h overview") { price: chart.panel(h4), price: table.panel(columns: 2) } plot c = plot.line(title: "4h close", on: w.price) on h4.close { c.plot(h4.close) } ``` ## Fixed example ```flowscope script "Four-hour window" data h4 = subscribe(data.ohlcv, timeframe: 4h) window w = window(title: "4h overview") { price: chart.panel(h4), stats: table.panel(columns: 2) } plot c = plot.line(title: "4h close", on: w.price) on h4.close { c.plot(h4.close) w.stats.clear() w.stats.row("Close", h4.close) } ``` ## Related - [T002](/docs/scripting/reference/diagnostics/t002) - [T011](/docs/scripting/reference/diagnostics/t011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s006 --- # S007 > constructor does not fit the declaration keyword **Severity** Error Each declaration keyword expects a particular constructor after `=`: `data` takes `subscribe(...)`, `plot` takes a `plot.*` constructor, `pane` takes `pane(...)`, `alert` takes `alert(...)`, `fill` takes `fill.between(...)` and `input` takes an `input.*` call. The right-hand side here belongs to a different kind of declaration. Use the keyword that matches the constructor, or the constructor that matches the keyword. ## Failing example ```flowscope script "OI line" plot oi = subscribe(data.oi) on oi.close { log.info(str.formatVolume(oi.close ?? 0.0)) } ``` ## Fixed example ```flowscope script "OI line" data oi = subscribe(data.oi) on oi.close { log.info(str.formatVolume(oi.close ?? 0.0)) } ``` ## Related - [S019](/docs/scripting/reference/diagnostics/s019) - [T405](/docs/scripting/reference/diagnostics/t405) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s007 --- # S008 > user function called in setup **Severity** Error User `fn`s may read subscriptions, state and the strategy, so they only run inside handlers. A setup value, input default or declaration argument called one. Write the expression directly in the setup value (setup-safe built-ins and the ternary operator are fine there), or compute the value in a handler and keep it in `state`. ## Failing example ```flowscope script "Tick offset" data chart = subscribe(data.ohlcv) fn ticks(n: float) -> float { return n * context.tickSize } setup offset = ticks(4.0) plot above = plot.line(title: "High + 4 ticks") on chart.close { above.plot(chart.high + offset) } ``` ## Fixed example ```flowscope script "Tick offset" data chart = subscribe(data.ohlcv) setup offset = 4.0 * context.tickSize plot above = plot.line(title: "High + 4 ticks") on chart.close { above.plot(chart.high + offset) } ``` ## Related - [S001](/docs/scripting/reference/diagnostics/s001) - [S002](/docs/scripting/reference/diagnostics/s002) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s008 --- # S011 > setup value reads a later declaration **Severity** Error Setup values are evaluated top to bottom in source order. This expression uses a binding that is declared further down the file, so its value does not exist yet when the expression runs. Move the referenced declaration above its first use. ## Failing example ```flowscope script "Tick band" data chart = subscribe(data.ohlcv) setup band = width * context.tickSize input width = input.int(10, title: "Band width (ticks)", min: 1) plot upper = plot.line(title: "Close + band") on chart.close { upper.plot(chart.close + band) } ``` ## Fixed example ```flowscope script "Tick band" data chart = subscribe(data.ohlcv) input width = input.int(10, title: "Band width (ticks)", min: 1) setup band = float(width) * context.tickSize plot upper = plot.line(title: "Close + band") on chart.close { upper.plot(chart.close + band) } ``` ## Related - [T001](/docs/scripting/reference/diagnostics/t001) - [S001](/docs/scripting/reference/diagnostics/s001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s011 --- # S012 > empty exchange list in subscription **Severity** Error A subscription's `exchange:` argument resolves to an empty list, so there is no venue to stream from. This happens most often with an `input.exchanges` whose default is empty. Give the list at least one venue, for example `["binance-futures", "bybit"]`, or pass `"all"` for the aggregated market. ## Failing example ```flowscope script "Venue CVD" input venues = input.exchanges([], title: "Venues") data cvd = subscribe(data.cvd, exchange: venues, bucket: "day") pane p = pane(title: "CVD", height: 0.25) plot line = plot.line(title: "Daily CVD", on: p) on cvd.close { line.plot(cvd.close) } ``` ## Fixed example ```flowscope script "Venue CVD" input venues = input.exchanges(["binance-futures", "bybit"], title: "Venues") data cvd = subscribe(data.cvd, exchange: venues, bucket: "day") pane p = pane(title: "CVD", height: 0.25) plot line = plot.line(title: "Daily CVD", on: p) on cvd.close { line.plot(cvd.close) } ``` ## Related - [S026](/docs/scripting/reference/diagnostics/s026) - [L006](/docs/scripting/reference/diagnostics/l006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s012 --- # S013 > color scale needs matching lists with at least two entries **Severity** Error `color.scale(positions:, colors:)` maps a number to a color by interpolating between stops. It needs at least two stops, and the two lists must have the same length so each position has exactly one color. Add the missing entry to the shorter list, or remove the extra one from the longer list. ## Failing example ```flowscope script "Imbalance heat" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) setup heat = color.scale(positions: [-1.0, 0.0, 1.0], colors: [color.bear, color.bull]) plot bg = plot.bg(title: "Book imbalance") on chart.update { let d = book.depth(1.0) if d != null { bg.plot(1.0, color: color.withAlpha(heat.at(d.imbalance), 60)) } } ``` ## Fixed example ```flowscope script "Imbalance heat" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) setup heat = color.scale(positions: [-1.0, 0.0, 1.0], colors: [color.bear, color.muted, color.bull]) plot bg = plot.bg(title: "Book imbalance") on chart.update { let d = book.depth(1.0) if d != null { bg.plot(1.0, color: color.withAlpha(heat.at(d.imbalance), 60)) } } ``` ## Related - [S002](/docs/scripting/reference/diagnostics/s002) - [T425](/docs/scripting/reference/diagnostics/t425) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s013 --- # S015 > minimum trade size must be positive and finite **Severity** Error The `minSize:` (base units) or `minNotional:` (USD) filter of a `data.trades` subscription must be a finite number greater than zero. A zero or negative threshold would not filter anything and usually signals a mistake. Use a positive threshold, or omit the argument to receive every trade. ## Failing example ```flowscope script "Whale prints" data tape = subscribe(data.trades, minNotional: 0.0) plot whale = plot.point(title: "Large print", shape: shape.circle, color: color.poc) on tape.trade(tr) { whale.plot(tr.price) } ``` ## Fixed example ```flowscope script "Whale prints" data tape = subscribe(data.trades, minNotional: 1_000_000.0) plot whale = plot.point(title: "Large print", shape: shape.circle, color: color.poc) on tape.trade(tr) { whale.plot(tr.price) } ``` ## Related - [S026](/docs/scripting/reference/diagnostics/s026) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s015 --- # S018 > lambda or collection transform in setup **Severity** Error Setup values and `state` initializers are computed once before any data arrives, and they are restricted to simple expressions. `map`, `filter` and `reduce` (and lambdas in general) are only available inside handlers and functions. Write the resulting literal directly, or keep the source data in setup and run the transform in a handler. ## Failing example ```flowscope script "Size thresholds" data tape = subscribe(data.trades, minNotional: 10_000.0) setup thresholds = [10.0, 100.0, 1000.0].map(v => v * 1000.0) state counts = float[](3) on tape.trade(tr) { if counts.len == 0 { for _ in thresholds { counts.push(0.0) } } for i in 0..3 { if tr.notional >= (thresholds[i] ?? 0.0) { counts[i] = (counts[i] ?? 0.0) + 1.0 } } } ``` ## Fixed example ```flowscope script "Size thresholds" data tape = subscribe(data.trades, minNotional: 10_000.0) setup thresholds = [10_000.0, 100_000.0, 1_000_000.0] state counts = float[](3) on tape.trade(tr) { if counts.len == 0 { for _ in thresholds { counts.push(0.0) } } for i in 0..3 { if tr.notional >= (thresholds[i] ?? 0.0) { counts[i] = (counts[i] ?? 0.0) + 1.0 } } } ``` ## Related - [T426](/docs/scripting/reference/diagnostics/t426) - [T430](/docs/scripting/reference/diagnostics/t430) - [S002](/docs/scripting/reference/diagnostics/s002) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s018 --- # S019 > alert constructor outside an alert declaration **Severity** Error `alert(...)` does not create a value you can pass around; it only declares an alert, and it is allowed solely as the right-hand side of an `alert` declaration. Using it in `setup`, in a local or inside a handler is rejected. Declare the alert at the top level with `alert name = alert(...)` and call `name.trigger()` from a handler. ## Failing example ```flowscope script "Funding flip" data funding = subscribe(data.funding) setup flip = alert(title: "Funding changed sign", onClose: true) on funding.close { let now = funding.rate ?? 0.0 let before = funding.rate[1] ?? 0.0 if now * before < 0.0 { flip.trigger() } } ``` ## Fixed example ```flowscope script "Funding flip" data funding = subscribe(data.funding) alert flip = alert(title: "Funding changed sign", onClose: true) on funding.close { let now = funding.rate ?? 0.0 let before = funding.rate[1] ?? 0.0 if now * before < 0.0 { flip.trigger() } } ``` ## Related - [S007](/docs/scripting/reference/diagnostics/s007) - [S020](/docs/scripting/reference/diagnostics/s020) - [S022](/docs/scripting/reference/diagnostics/s022) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s019 --- # S020 > invalid alert payload type **Severity** Error `payload:` must name a user `type` with at least one field, and every field must be `string`, `int`, `float`, `bool` or a nullable version of these. Payloads are serialised into the notification, so colors, times, collections and nested records cannot be carried. Convert such fields to one of the allowed types (for example format a time with `time.format` into a string), or drop `payload:` for an alert without data. ## Failing example ```flowscope script "Liquidation alert" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) type LiqHit { usd: float tint: color } alert big = alert(title: "Large liquidation bar", payload: LiqHit) on chart.close { let total = liq.total ?? 0.0 if total > 5_000_000.0 { big.trigger(LiqHit { usd: total, tint: color.bear }) } } ``` ## Fixed example ```flowscope script "Liquidation alert" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) type LiqHit { usd: float side: string } alert big = alert(title: "Large liquidation bar", payload: LiqHit) on chart.close { let longs = liq.longs ?? 0.0 let shorts = liq.shorts ?? 0.0 if longs + shorts > 5_000_000.0 { big.trigger(LiqHit { usd: longs + shorts, side: longs > shorts ? "longs" : "shorts" }) } } ``` ## Related - [S022](/docs/scripting/reference/diagnostics/s022) - [L013](/docs/scripting/reference/diagnostics/l013) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s020 --- # S022 > trigger argument does not match the alert payload **Severity** Error An alert declared with `payload: T` must be triggered with exactly one value of type `T`; an alert without a payload is triggered with no argument. The call here passes the wrong number of values. Match the declaration: add the payload record, remove the stray argument, or change the declaration. ## Failing example ```flowscope script "Funding extreme" data funding = subscribe(data.funding) type FundingHit { rate: float } alert extreme = alert(title: "Extreme funding", payload: FundingHit) on funding.close { let r = funding.rate ?? 0.0 if math.abs(r) > 0.001 { extreme.trigger() } } ``` ## Fixed example ```flowscope script "Funding extreme" data funding = subscribe(data.funding) type FundingHit { rate: float } alert extreme = alert(title: "Extreme funding", payload: FundingHit) on funding.close { let r = funding.rate ?? 0.0 if math.abs(r) > 0.001 { extreme.trigger(FundingHit { rate: r }) } } ``` ## Related - [S020](/docs/scripting/reference/diagnostics/s020) - [T204](/docs/scripting/reference/diagnostics/t204) - [T205](/docs/scripting/reference/diagnostics/t205) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s022 --- # S026 > subscription history cannot be negative **Severity** Error `history:` requests earlier data, either as a number of periods or as a duration such as `30D`. A negative value has no meaning. Use a positive count or duration. `0s` asks for live data only (not allowed for the chart's own subscription), and omitting the argument uses the default warm-up. ## Failing example ```flowscope script "Daily open interest" data oi = subscribe(data.oi, timeframe: 1D, history: -90) pane p = pane(title: "Daily OI", height: 0.25) plot line = plot.line(title: "OI (1D)", on: p) on oi.close { line.plot(oi.close) } ``` ## Fixed example ```flowscope script "Daily open interest" data oi = subscribe(data.oi, timeframe: 1D, history: 90D) pane p = pane(title: "Daily OI", height: 0.25) plot line = plot.line(title: "OI (1D)", on: p) on oi.close { line.plot(oi.close) } ``` ## Related - [S027](/docs/scripting/reference/diagnostics/s027) - [S015](/docs/scripting/reference/diagnostics/s015) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s026 --- # S027 > the chart subscription cannot be live only **Severity** Error An `ohlcv` subscription that inherits exchange, symbol and timeframe from the chart defines the bars the script is replayed over. Asking it for live data only (`history: 0s`) would leave the script with no history to replay. Remove `history:` from the chart subscription. If you need a live-only stream, give it its own timeframe or market. ## Failing example ```flowscope script "Live close" data chart = subscribe(data.ohlcv, history: 0s) plot c = plot.line(title: "Close") on chart.update { c.plot(chart.close) } ``` ## Fixed example ```flowscope script "Live close" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.update { c.plot(chart.close) } ``` ## Related - [S026](/docs/scripting/reference/diagnostics/s026) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s027 --- # S028 > leverage bound below 1 **Severity** Error `minLeverage:` and `maxLeverage:` of a `data.liqmap` subscription name leverage tiers (5x to 100x): a bound below 1 (no leverage) has no meaning. Use a tier such as `25.0`, or leave the bound out. ## Failing example ```flowscope script "conf" data chart = subscribe(data.ohlcv) data liq = subscribe(data.liqmap, minLeverage: 0.5) on chart.close { log.info(str.format("{0}", liq.densityBelow(2.0))) } ``` ## Fixed example ```flowscope script "conf" data chart = subscribe(data.ohlcv) data liq = subscribe(data.liqmap, minLeverage: 25.0) on chart.close { log.info(str.format("{0}", liq.densityBelow(2.0))) } ``` ## Related - [S026](/docs/scripting/reference/diagnostics/s026) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/s028 --- # T001 > unknown identifier **Severity** Error A name is used that is neither declared in the current scope nor a built-in. Typical causes are typos, a binding declared inside another block, or habits from other languages, such as a bare `close` instead of a field of a subscription. Declare the name, fix its spelling, or read the value through its subscription (`chart.close`). The message suggests the closest known name when there is one. ## Failing example ```flowscope script "EMA 50" data chart = subscribe(data.ohlcv) plot ema = plot.line(title: "EMA 50", color: color.accent) on chart.close { ema.plot(ta.ema(close, 50)) } ``` ## Fixed example ```flowscope script "EMA 50" data chart = subscribe(data.ohlcv) plot ema = plot.line(title: "EMA 50", color: color.accent) on chart.close { ema.plot(ta.ema(chart.close, 50)) } ``` ## Related - [T005](/docs/scripting/reference/diagnostics/t005) - [S011](/docs/scripting/reference/diagnostics/s011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t001 --- # T002 > duplicate top-level name **Severity** Error Two top-level declarations introduce the same name. Inputs, subscriptions, state, setup values, plots, panes, windows, alerts, types and functions all share one namespace. Rename one of them so that every top-level binding is unique. ## Failing example ```flowscope script "Two averages" data chart = subscribe(data.ohlcv) plot avg = plot.line(title: "SMA 20", color: color.cyan) plot avg = plot.line(title: "SMA 100", color: color.violet) on chart.close { avg.plot(ta.sma(chart.close, 20)) } ``` ## Fixed example ```flowscope script "Two averages" data chart = subscribe(data.ohlcv) plot fast = plot.line(title: "SMA 20", color: color.cyan) plot slow = plot.line(title: "SMA 100", color: color.violet) on chart.close { fast.plot(ta.sma(chart.close, 20)) slow.plot(ta.sma(chart.close, 100)) } ``` ## Related - [T007](/docs/scripting/reference/diagnostics/t007) - [T011](/docs/scripting/reference/diagnostics/t011) - [S006](/docs/scripting/reference/diagnostics/s006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t002 --- # T005 > unknown type name **Severity** Error A type annotation names a type that is neither built in (`int`, `float`, `bool`, `string`, `color`, `time`, `duration`, `timeframe`, `session`, the record types of data sources) nor declared with `type` in the script. Correct the spelling, or declare the type. Note that there is a single floating-point type, `float`. ## Failing example ```flowscope script "Session peak" data chart = subscribe(data.ohlcv) state peak: double = 0.0 plot p = plot.line(title: "Peak") on chart.close { peak = math.max(peak, chart.high ?? peak) p.plot(peak) } ``` ## Fixed example ```flowscope script "Session peak" data chart = subscribe(data.ohlcv) state peak: float = 0.0 plot p = plot.line(title: "Peak") on chart.close { peak = math.max(peak, chart.high ?? peak) p.plot(peak) } ``` ## Related - [T001](/docs/scripting/reference/diagnostics/t001) - [T303](/docs/scripting/reference/diagnostics/t303) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t005 --- # T006 > local hides a built-in namespace **Severity** Error A local, parameter or loop variable is named like a built-in namespace (`ta`, `math`, `color`, `str`, `time`, `plot`, `draw`, `strategy` and so on). Within its scope the namespace would become unreachable, so calls such as `color.bull` would suddenly mean something else. Rename the binding, for example `tint` instead of `color`. ## Failing example ```flowscope script "Candle tint" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { let color = chart.close > chart.open ? color.bull : color.bear c.plot(chart.close, color: color) } ``` ## Fixed example ```flowscope script "Candle tint" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { let tint = chart.close > chart.open ? color.bull : color.bear c.plot(chart.close, color: tint) } ``` ## Related - [P025](/docs/scripting/reference/diagnostics/p025) - [T007](/docs/scripting/reference/diagnostics/t007) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t006 --- # T007 > name is already bound in this scope **Severity** Error A `let`, parameter or loop variable reuses a name that is already visible in the same scope. Because `let` bindings are immutable, a second `let` with the same name would hide the first one and make the code hard to follow. Pick a new name for the second binding. If the value should change over time, use a `state` variable and assign it. ## Failing example ```flowscope script "Wick sizes" data chart = subscribe(data.ohlcv) pane p = pane(title: "Wicks", height: 0.2) plot wick = plot.histogram(title: "Wick total", on: p) on chart.close { let w = chart.high - math.max(chart.open ?? 0.0, chart.close ?? 0.0) let w = w + math.min(chart.open ?? 0.0, chart.close ?? 0.0) - chart.low wick.plot(w) } ``` ## Fixed example ```flowscope script "Wick sizes" data chart = subscribe(data.ohlcv) pane p = pane(title: "Wicks", height: 0.2) plot wick = plot.histogram(title: "Wick total", on: p) on chart.close { let upper = chart.high - math.max(chart.open ?? 0.0, chart.close ?? 0.0) let total = upper + math.min(chart.open ?? 0.0, chart.close ?? 0.0) - chart.low wick.plot(total) } ``` ## Related - [T002](/docs/scripting/reference/diagnostics/t002) - [T216](/docs/scripting/reference/diagnostics/t216) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t007 --- # T010 > script header missing or misplaced **Severity** Error Every Flowscope Script file must start with exactly one `script "Name"` line, before any declaration. Only blank lines and comments may come before it. The header gives the script its default title in the indicator list and in the Strategy Tester. Add the header as the first line, or move the existing one to the top and remove duplicates. ## Failing example ```flowscope data chart = subscribe(data.ohlcv) script "Typical price" plot tp = plot.line(title: "Typical price") on chart.close { tp.plot((chart.high + chart.low + chart.close) / 3.0) } ``` ## Fixed example ```flowscope script "Typical price" data chart = subscribe(data.ohlcv) plot tp = plot.line(title: "Typical price") on chart.close { tp.plot((chart.high + chart.low + chart.close) / 3.0) } ``` ## Related - [P011](/docs/scripting/reference/diagnostics/p011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t010 --- # T011 > duplicate display title **Severity** Warning Two declarations show the same title to the user. Plot, fill, pane, window and alert titles must be unique across the script, because they label the legend, the style dialog and alert notifications. Input titles only need to be unique within their tab, section or group. Give each element a distinct title. The script still runs, but the two entries cannot be told apart in the UI. ## Failing example ```flowscope script "EMA ribbon" data chart = subscribe(data.ohlcv) plot fast = plot.line(title: "EMA", color: color.cyan) plot slow = plot.line(title: "EMA", color: color.violet) on chart.close { fast.plot(ta.ema(chart.close, 21)) slow.plot(ta.ema(chart.close, 55)) } ``` ## Fixed example ```flowscope script "EMA ribbon" data chart = subscribe(data.ohlcv) plot fast = plot.line(title: "EMA 21", color: color.cyan) plot slow = plot.line(title: "EMA 55", color: color.violet) on chart.close { fast.plot(ta.ema(chart.close, 21)) slow.plot(ta.ema(chart.close, 55)) } ``` ## Related - [T002](/docs/scripting/reference/diagnostics/t002) - [T016](/docs/scripting/reference/diagnostics/t016) - [S006](/docs/scripting/reference/diagnostics/s006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t011 --- # T013 > unused binding **Severity** Warning A `let` local or `state` variable is assigned but never read. Often it is left over from an edit, or a different variable was used by mistake further down. Remove the binding or use it. If you call something only for its side effect, use the call as a statement, or bind it to `_`. ## Failing example ```flowscope script "Bar delta" data chart = subscribe(data.ohlcv) pane p = pane(title: "Delta", height: 0.25) plot delta = plot.columns(title: "Delta", on: p) on chart.close { let range = chart.high - chart.low delta.plot(chart.buyVolume - chart.sellVolume) } ``` ## Fixed example ```flowscope script "Bar delta" data chart = subscribe(data.ohlcv) pane p = pane(title: "Delta", height: 0.25) plot delta = plot.columns(title: "Delta", on: p) on chart.close { delta.plot(chart.buyVolume - chart.sellVolume) } ``` ## Related - [T014](/docs/scripting/reference/diagnostics/t014) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t013 --- # T014 > unused subscription **Severity** Warning A `data` subscription is declared but neither read nor driving a handler. Flowscope still has to load its history and keep its stream open, which costs memory and start-up time for nothing. Remove the subscription, or use it: read its fields in a handler or attach a handler to it. ## Failing example ```flowscope script "Close only" data ( chart = subscribe(data.ohlcv) oi = subscribe(data.oi) ) plot c = plot.line(title: "Close") on chart.close { c.plot(chart.close) } ``` ## Fixed example ```flowscope script "Close only" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { c.plot(chart.close) } ``` ## Related - [T013](/docs/scripting/reference/diagnostics/t013) - [L006](/docs/scripting/reference/diagnostics/l006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t014 --- # T016 > duplicate input key **Severity** Error Two inputs end up with the same key. The key (the explicit `key:` argument, or else the binding name) identifies the input in saved layouts and parameter overrides, so a second input with the same key could never be set on its own. Give each input a distinct key, or remove `key:` so the binding names are used. ## Failing example ```flowscope script "Two lengths" input ( fastLen = input.int(9, title: "Fast length", key: "len") slowLen = input.int(21, title: "Slow length", key: "len") ) data chart = subscribe(data.ohlcv) plot fast = plot.line(title: "Fast EMA") plot slow = plot.line(title: "Slow EMA") on chart.close { fast.plot(ta.ema(chart.close, fastLen)) slow.plot(ta.ema(chart.close, slowLen)) } ``` ## Fixed example ```flowscope script "Two lengths" input ( fastLen = input.int(9, title: "Fast length", key: "fastLen") slowLen = input.int(21, title: "Slow length", key: "slowLen") ) data chart = subscribe(data.ohlcv) plot fast = plot.line(title: "Fast EMA") plot slow = plot.line(title: "Slow EMA") on chart.close { fast.plot(ta.ema(chart.close, fastLen)) slow.plot(ta.ema(chart.close, slowLen)) } ``` ## Related - [T011](/docs/scripting/reference/diagnostics/t011) - [T002](/docs/scripting/reference/diagnostics/t002) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t016 --- # T101 > type mismatch **Severity** Error A value of one type is used where another type is required: a string passed as a number, a float assigned to a `string` state, a color where a bool is expected and so on. Flowscope Script never converts between unrelated types implicitly. Pass a value of the expected type, or convert explicitly with `int(x)`, `float(x)` or `string(x)`. The message names both the expected and the actual type. ## Failing example ```flowscope script "Thick VWAP" data chart = subscribe(data.ohlcv) plot vwap = plot.line(title: "VWAP", color: color.poc, width: "3") on chart.close { vwap.plot(ta.vwap(chart)) } ``` ## Fixed example ```flowscope script "Thick VWAP" data chart = subscribe(data.ohlcv) plot vwap = plot.line(title: "VWAP", color: color.poc, width: 3) on chart.close { vwap.plot(ta.vwap(chart)) } ``` ## Related - [T102](/docs/scripting/reference/diagnostics/t102) - [T103](/docs/scripting/reference/diagnostics/t103) - [T215](/docs/scripting/reference/diagnostics/t215) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t101 --- # T102 > operator not defined for these operand types **Severity** Error The operator is applied to operand types it does not support, for example adding two colors, multiplying a string, or comparing a bool with `<`. Arithmetic works on `int` and `float`, `+` also joins two strings, and time values combine with durations. Use the dedicated function for the job, such as `color.mix` for blending or `str.format` for text, or convert the operands first. ## Failing example ```flowscope script "Blended line" data chart = subscribe(data.ohlcv) plot mid = plot.line(title: "Midpoint") on chart.close { let tint = color.bull + color.bear mid.plot((chart.high + chart.low) / 2.0, color: tint) } ``` ## Fixed example ```flowscope script "Blended line" data chart = subscribe(data.ohlcv) plot mid = plot.line(title: "Midpoint") on chart.close { let tint = color.mix(color.bull, color.bear, 0.5) mid.plot((chart.high + chart.low) / 2.0, color: tint) } ``` ## Related - [T101](/docs/scripting/reference/diagnostics/t101) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t102 --- # T103 > float needs an explicit conversion to int **Severity** Error A `float` value is used where an `int` is required. An `int` is widened to `float` automatically, but the reverse would silently drop the fractional part, so it always has to be written out. Round the value the way you mean it (`math.floor`, `math.round`, `math.ceil`) and convert with `int(...)`. `int` returns null for non-finite input, so add a fallback with `??` where a non-null `int` is needed. ## Failing example ```flowscope script "Tick index" data chart = subscribe(data.ohlcv) state level: int = 0 pane p = pane(title: "Ticks", height: 0.2) plot ticks = plot.line(title: "Close in ticks", on: p) on chart.close { let c = chart.close ?? 0.0 level = math.floor(c / context.tickSize) ticks.plot(level) } ``` ## Fixed example ```flowscope script "Tick index" data chart = subscribe(data.ohlcv) state level: int = 0 pane p = pane(title: "Ticks", height: 0.2) plot ticks = plot.line(title: "Close in ticks", on: p) on chart.close { let c = chart.close ?? 0.0 level = int(math.floor(c / context.tickSize)) ?? 0 ticks.plot(level) } ``` ## Related - [T101](/docs/scripting/reference/diagnostics/t101) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t103 --- # T106 > invalid map key type **Severity** Error Map keys must be `string`, `int` or `bool`. Floats are rejected because two prices that print the same can differ in their last bit, so lookups would miss; colors, times and records are not supported as keys either. For price-keyed maps, convert the price to an integer tick index (`int(math.round(price / context.tickSize))`) and use that as the key. ## Failing example ```flowscope script "Volume at price" data chart = subscribe(data.ohlcv) state byPrice = map(2000) on chart.close { let c = chart.close if c != null { byPrice[c] = (byPrice[c] ?? 0.0) + (chart.volume ?? 0.0) } } ``` ## Fixed example ```flowscope script "Volume at price" data chart = subscribe(data.ohlcv) state byTick = map(2000) on chart.close { let c = chart.close if c != null { let key = int(math.round(c / context.tickSize)) ?? 0 byTick[key] = (byTick[key] ?? 0.0) + (chart.volume ?? 0.0) } } ``` ## Related - [T101](/docs/scripting/reference/diagnostics/t101) - [C003](/docs/scripting/reference/diagnostics/c003) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t106 --- # T109 > type is not indexable **Severity** Error `[...]` was applied to a value that has no index operation. Subscription fields with history (`chart.close[3]`), arrays, rolling buffers, maps and strings can be indexed; plain values such as a trade's price, a `context.*` constant or a local number cannot. If you need earlier values of something that has no history, push it into a `rolling` buffer in `state` and index that. ## Failing example ```flowscope script "Trade acceleration" data tape = subscribe(data.trades, minNotional: 50_000.0) on tape.trade(tr) { if tr.size > tr.size[1] * 3.0 { log.info(str.format("size jump at {0}", tr.price)) } } ``` ## Fixed example ```flowscope script "Trade acceleration" data tape = subscribe(data.trades, minNotional: 50_000.0) state sizes = rolling(2) on tape.trade(tr) { let prev = sizes.last() if prev != null && tr.size > prev * 3.0 { log.info(str.format("size jump at {0}", tr.price)) } sizes.push(tr.size) } ``` ## Related - [T420](/docs/scripting/reference/diagnostics/t420) - [C006](/docs/scripting/reference/diagnostics/c006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t109 --- # T111 > invalid timeframe literal **Severity** Error A timeframe or duration literal uses an unknown unit or is malformed. Valid units are `s` (seconds), `m` (minutes), `h` (hours), `D` (days), `W` (weeks) and `M` (months), always with an uppercase `D`, `W` and `M`; for example `30s`, `15m`, `4h`, `1D`, `1W`. Correct the unit. If the timeframe comes from text, use `tf("15m")`. ## Failing example ```flowscope script "Daily close" data daily = subscribe(data.ohlcv, timeframe: 1d) plot c = plot.line(title: "Daily close", style: linestyle.step) on daily.close { c.plot(daily.close) } ``` ## Fixed example ```flowscope script "Daily close" data daily = subscribe(data.ohlcv, timeframe: 1D) plot c = plot.line(title: "Daily close", style: linestyle.step) on daily.close { c.plot(daily.close) } ``` ## Related - [T112](/docs/scripting/reference/diagnostics/t112) - [T408](/docs/scripting/reference/diagnostics/t408) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t111 --- # T112 > zero-length timeframe **Severity** Error A timeframe literal has a magnitude of zero, such as `0m`. Bars need a positive period, so a zero timeframe cannot exist. A zero duration (for example `history: 0s`) is valid where a duration is expected. Use a positive timeframe. ## Failing example ```flowscope script "Fast bars" data fast = subscribe(data.ohlcv, timeframe: 0m) plot c = plot.line(title: "Fast close") on fast.close { c.plot(fast.close) } ``` ## Fixed example ```flowscope script "Fast bars" data fast = subscribe(data.ohlcv, timeframe: 1m) plot c = plot.line(title: "Fast close") on fast.close { c.plot(fast.close) } ``` ## Related - [T111](/docs/scripting/reference/diagnostics/t111) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t112 --- # T113 > integer literal out of range **Severity** Error An integer literal does not fit into the 64-bit signed `int` type (roughly ±9.2 × 10¹⁸). Use a smaller value, or write a `float` literal (`1.0e20`) if precision beyond 15 digits is not needed. ## Failing example ```flowscope script "Big counter" data chart = subscribe(data.ohlcv) state limit: int = 99_999_999_999_999_999_999 on chart.close { if chart.index > limit { log.warn("limit reached") } } ``` ## Fixed example ```flowscope script "Big counter" data chart = subscribe(data.ohlcv) state limit: int = 9_000_000_000 on chart.close { if chart.index > limit { log.warn("limit reached") } } ``` ## Related - [T101](/docs/scripting/reference/diagnostics/t101) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t113 --- # T114 > invalid session literal **Severity** Error A session string must have the form `"HHMM-HHMM"` in 24-hour time, with hours from 00 to 24 and minutes from 00 to 59. Here an hour or minute is out of range or the format is wrong. Correct the times. Sessions crossing midnight are written with the end earlier than the start, for example `"2200-0600"`. ## Failing example ```flowscope script "London session" data chart = subscribe(data.ohlcv) setup london = session("0800-2600") plot bg = plot.bg(title: "London hours") on chart.close { let t = chart.time if t != null && time.inSession(t, london, "Europe/London") { bg.plot(1.0, color: color.withAlpha(color.accent, 30)) } } ``` ## Fixed example ```flowscope script "London session" data chart = subscribe(data.ohlcv) setup london = session("0800-1630") plot bg = plot.bg(title: "London hours") on chart.close { let t = chart.time if t != null && time.inSession(t, london, "Europe/London") { bg.plot(1.0, color: color.withAlpha(color.accent, 30)) } } ``` ## Related - [T111](/docs/scripting/reference/diagnostics/t111) - [T416](/docs/scripting/reference/diagnostics/t416) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t114 --- # T201 > unknown named argument **Severity** Error A call passes a named argument that the function does not have. Named arguments must use the exact parameter name from the signature. Correct the name (the message lists the parameters that exist), or pass the value positionally. ## Failing example ```flowscope script "Thick SMA" data chart = subscribe(data.ohlcv) plot sma = plot.line(title: "SMA 200", thickness: 2) on chart.close { sma.plot(ta.sma(chart.close, 200)) } ``` ## Fixed example ```flowscope script "Thick SMA" data chart = subscribe(data.ohlcv) plot sma = plot.line(title: "SMA 200", width: 2) on chart.close { sma.plot(ta.sma(chart.close, 200)) } ``` ## Related - [T203](/docs/scripting/reference/diagnostics/t203) - [T215](/docs/scripting/reference/diagnostics/t215) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t201 --- # T202 > positional argument after a named one **Severity** Error Once an argument is passed by name, all following arguments must be named too; otherwise it would be unclear which parameter a positional value belongs to. Move positional arguments to the front, or name every argument after the first named one. ## Failing example ```flowscope script "Configurable RSI" input len = input.int(title: "RSI length", 14, min: 2) data chart = subscribe(data.ohlcv) pane p = pane(title: "RSI", height: 0.25) plot rsi = plot.line(title: "RSI", on: p) on chart.close { rsi.plot(ta.rsi(chart.close, len)) } ``` ## Fixed example ```flowscope script "Configurable RSI" input len = input.int(14, title: "RSI length", min: 2) data chart = subscribe(data.ohlcv) pane p = pane(title: "RSI", height: 0.25) plot rsi = plot.line(title: "RSI", on: p) on chart.close { rsi.plot(ta.rsi(chart.close, len)) } ``` ## Related - [T203](/docs/scripting/reference/diagnostics/t203) - [T205](/docs/scripting/reference/diagnostics/t205) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t202 --- # T203 > argument supplied more than once **Severity** Error The same parameter receives a value twice, either through a positional value and a named argument, or through two named arguments with the same name. Remove one of them. ## Failing example ```flowscope script "Slow average" data chart = subscribe(data.ohlcv) plot avg = plot.line(title: "SMA") on chart.close { avg.plot(ta.sma(chart.close, 20, length: 50)) } ``` ## Fixed example ```flowscope script "Slow average" data chart = subscribe(data.ohlcv) plot avg = plot.line(title: "SMA") on chart.close { avg.plot(ta.sma(chart.close, 50)) } ``` ## Related - [T202](/docs/scripting/reference/diagnostics/t202) - [T205](/docs/scripting/reference/diagnostics/t205) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t203 --- # T204 > missing argument **Severity** Error A required parameter, one without a default value, was not supplied. Pass the missing argument. The catalog shows every parameter with its default; only those without a default are required. ## Failing example ```flowscope script "Bollinger" data chart = subscribe(data.ohlcv) plot upper = plot.line(title: "Upper band") plot lower = plot.line(title: "Lower band") on chart.close { let (mid, up, lo) = ta.bb(chart.close, 20) upper.plot(up) lower.plot(lo) log.debug(str.format("mid {0}", mid ?? 0.0)) } ``` ## Fixed example ```flowscope script "Bollinger" data chart = subscribe(data.ohlcv) plot upper = plot.line(title: "Upper band") plot lower = plot.line(title: "Lower band") on chart.close { let (mid, up, lo) = ta.bb(chart.close, 20, 2.0) upper.plot(up) lower.plot(lo) log.debug(str.format("mid {0}", mid ?? 0.0)) } ``` ## Related - [T205](/docs/scripting/reference/diagnostics/t205) - [T215](/docs/scripting/reference/diagnostics/t215) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t204 --- # T205 > too many positional values **Severity** Error A call passes more positional arguments than the function accepts, or a loop or destructuring header lists more names than there are values. Remove the extra values. If you meant to set an optional parameter, pass it by name. ## Failing example ```flowscope script "RSI with level" data chart = subscribe(data.ohlcv) pane p = pane(title: "RSI", height: 0.25) plot rsi = plot.line(title: "RSI 14", on: p) on chart.close { rsi.plot(ta.rsi(chart.close, 14, 70.0)) } ``` ## Fixed example ```flowscope script "RSI with level" data chart = subscribe(data.ohlcv) pane p = pane(title: "RSI", height: 0.25) plot rsi = plot.line(title: "RSI 14", on: p) plot ob = plot.hline(70.0, title: "Overbought", style: linestyle.dashed, on: p) on chart.close { rsi.plot(ta.rsi(chart.close, 14)) } ``` ## Related - [T204](/docs/scripting/reference/diagnostics/t204) - [T214](/docs/scripting/reference/diagnostics/t214) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t205 --- # T206 > parameter default is not a literal **Severity** Error Default values of `fn` parameters must be literals (`14`, `2.0`, `"day"`, `true`, `null`, a color literal). Reading other bindings or calling functions in a default is not allowed, because defaults are fixed when the function is declared. Use a literal default, or make the parameter nullable with a `null` default and compute the real value inside the body. ## Failing example ```flowscope script "Tick offsets" data chart = subscribe(data.ohlcv) fn offset(ticks: float, size: float = context.tickSize) -> float { return ticks * size } plot above = plot.line(title: "High + 10 ticks") on chart.close { above.plot(chart.high + offset(10.0)) } ``` ## Fixed example ```flowscope script "Tick offsets" data chart = subscribe(data.ohlcv) fn offset(ticks: float, size: float? = null) -> float { return ticks * (size ?? context.tickSize) } plot above = plot.line(title: "High + 10 ticks") on chart.close { above.plot(chart.high + offset(10.0)) } ``` ## Related - [T207](/docs/scripting/reference/diagnostics/t207) - [T208](/docs/scripting/reference/diagnostics/t208) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t206 --- # T207 > required parameter after an optional one **Severity** Error A parameter without a default follows a parameter that has one. Positional calls would then be ambiguous, so all required parameters must come first. Reorder the parameters, or give the later parameter a default as well. ## Failing example ```flowscope script "Scaled delta" data vd = subscribe(data.vd) fn scaled(x: float, factor: float = 1.0, offset: float) -> float { return x * factor + offset } pane p = pane(title: "Delta", height: 0.2) plot d = plot.columns(title: "Delta (k)", on: p) on vd.close { d.plot(scaled(vd.close ?? 0.0, 0.001, 0.0)) } ``` ## Fixed example ```flowscope script "Scaled delta" data vd = subscribe(data.vd) fn scaled(x: float, offset: float, factor: float = 1.0) -> float { return x * factor + offset } pane p = pane(title: "Delta", height: 0.2) plot d = plot.columns(title: "Delta (k)", on: p) on vd.close { d.plot(scaled(vd.close ?? 0.0, 0.0, factor: 0.001)) } ``` ## Related - [T206](/docs/scripting/reference/diagnostics/t206) - [T208](/docs/scripting/reference/diagnostics/t208) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t207 --- # T208 > null default for a non-nullable parameter **Severity** Error A parameter is declared with a non-nullable type but `null` as its default. The default would violate the parameter's own type. Make the type nullable (`float?`) and handle the null case in the body, or choose a concrete default. ## Failing example ```flowscope script "Capped delta" data vd = subscribe(data.vd) fn capped(x: float, cap: float = null) -> float { return math.clamp(x, -cap, cap) } pane p = pane(title: "Delta", height: 0.2) plot d = plot.columns(title: "Capped delta", on: p) on vd.close { d.plot(capped(vd.close ?? 0.0)) } ``` ## Fixed example ```flowscope script "Capped delta" data vd = subscribe(data.vd) fn capped(x: float, cap: float? = null) -> float { let c = cap ?? 1_000_000.0 return math.clamp(x, -c, c) } pane p = pane(title: "Delta", height: 0.2) plot d = plot.columns(title: "Capped delta", on: p) on vd.close { d.plot(capped(vd.close ?? 0.0)) } ``` ## Related - [T206](/docs/scripting/reference/diagnostics/t206) - [N003](/docs/scripting/reference/diagnostics/n003) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t208 --- # T211 > wrong number of return values **Severity** Error A `return` hands back a different number of values than the function signature declares: one value in a function declared `-> (float, float)`, two values in a single-value function, or any value at all in a handler or a function without a return type. Return exactly the declared values, or change the signature. ## Failing example ```flowscope script "Range bounds" data chart = subscribe(data.ohlcv) fn bounds(len: int) -> (float?, float?) { return ta.highest(chart.high, len) } plot hi = plot.line(title: "20-bar high") plot lo = plot.line(title: "20-bar low") on chart.close { let (h, l) = bounds(20) hi.plot(h) lo.plot(l) } ``` ## Fixed example ```flowscope script "Range bounds" data chart = subscribe(data.ohlcv) fn bounds(len: int) -> (float?, float?) { return ta.highest(chart.high, len), ta.lowest(chart.low, len) } plot hi = plot.line(title: "20-bar high") plot lo = plot.line(title: "20-bar low") on chart.close { let (h, l) = bounds(20) hi.plot(h) lo.plot(l) } ``` ## Related - [T212](/docs/scripting/reference/diagnostics/t212) - [T214](/docs/scripting/reference/diagnostics/t214) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t211 --- # T212 > not every path returns a value **Severity** Error A function with a return type can reach its closing brace without a `return`, for example when only the `if` branch returns. Add a `return` on every path, typically a final `return` after the `if`, or an `else` branch. ## Failing example ```flowscope script "Delta sign" data vd = subscribe(data.vd) fn sign(x: float) -> float { if x >= 0.0 { return 1.0 } } pane p = pane(title: "Sign", height: 0.15) plot s = plot.columns(title: "Delta sign", on: p) on vd.close { s.plot(sign(vd.close ?? 0.0)) } ``` ## Fixed example ```flowscope script "Delta sign" data vd = subscribe(data.vd) fn sign(x: float) -> float { if x >= 0.0 { return 1.0 } return -1.0 } pane p = pane(title: "Sign", height: 0.15) plot s = plot.columns(title: "Delta sign", on: p) on vd.close { s.plot(sign(vd.close ?? 0.0)) } ``` ## Related - [T211](/docs/scripting/reference/diagnostics/t211) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t212 --- # T214 > destructuring does not match the returned values **Severity** Error `let (a, b) = f()` must list exactly one name per returned value. Here the number of names differs from the number of values the function returns, or the right-hand side returns a single value. Match the count. Use `_` for results you do not need, for example `let (line, _, hist) = ta.macd(chart.close, 12, 26, 9)`. ## Failing example ```flowscope script "MACD histogram" data chart = subscribe(data.ohlcv) pane p = pane(title: "MACD", height: 0.25) plot hist = plot.histogram(title: "Histogram", on: p) on chart.close { let (line, signal) = ta.macd(chart.close, 12, 26, 9) hist.plot(line - signal) } ``` ## Fixed example ```flowscope script "MACD histogram" data chart = subscribe(data.ohlcv) pane p = pane(title: "MACD", height: 0.25) plot hist = plot.histogram(title: "Histogram", on: p) on chart.close { let (_, _, h) = ta.macd(chart.close, 12, 26, 9) hist.plot(h) } ``` ## Related - [T211](/docs/scripting/reference/diagnostics/t211) - [T205](/docs/scripting/reference/diagnostics/t205) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t214 --- # T215 > no overload matches these arguments **Severity** Error The built-in has several forms, and none of them accepts this combination of argument types and count. A common example is calling `p.plot` with three values: line-style plots take one value, candle and bar plots take four. Compare the call with the signatures listed in the message and adjust the arguments. ## Failing example ```flowscope script "Heikin-style candles" data chart = subscribe(data.ohlcv) pane p = pane(title: "Smoothed candles", height: 0.35) plot bars = plot.candle(title: "Smoothed", on: p) on chart.close { let o = ta.ema(chart.open, 3) let c = ta.ema(chart.close, 3) bars.plot(o, chart.high, c) } ``` ## Fixed example ```flowscope script "Heikin-style candles" data chart = subscribe(data.ohlcv) pane p = pane(title: "Smoothed candles", height: 0.35) plot bars = plot.candle(title: "Smoothed", on: p) on chart.close { let o = ta.ema(chart.open, 3) let c = ta.ema(chart.close, 3) bars.plot(o, chart.high, chart.low, c) } ``` ## Related - [T101](/docs/scripting/reference/diagnostics/t101) - [T204](/docs/scripting/reference/diagnostics/t204) - [T415](/docs/scripting/reference/diagnostics/t415) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t215 --- # T216 > value cannot be reassigned **Severity** Error An assignment targets a binding that is immutable: a `let` local, an `input`, a `setup` value, a `data` subscription or a function. Only `state` variables (and their fields and elements) can change after initialisation. Declare the value as `state` if it has to change between bars, or introduce a new `let` with a different name for a derived value. ## Failing example ```flowscope script "Adaptive threshold" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) setup threshold = 1_000_000.0 plot hit = plot.marker(title: "Liq above threshold", shape: shape.diamond) on chart.close { let total = liq.total ?? 0.0 if total > threshold { hit.plot(chart.high) threshold *= 1.1 } } ``` ## Fixed example ```flowscope script "Adaptive threshold" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) state threshold: float = 1_000_000.0 plot hit = plot.marker(title: "Liq above threshold", shape: shape.diamond) on chart.close { let total = liq.total ?? 0.0 if total > threshold { hit.plot(chart.high) threshold *= 1.1 } } ``` ## Related - [T217](/docs/scripting/reference/diagnostics/t217) - [C001](/docs/scripting/reference/diagnostics/c001) - [T420](/docs/scripting/reference/diagnostics/t420) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t216 --- # T217 > invalid assignment target **Severity** Error The left side of `=` (or of `+=` and the other compound operators) is not something that can hold a value: a call result, an arithmetic expression or a literal. Only names, fields (`s.count`) and indexed elements (`xs[i]`) are assignable. Assign to the storage itself. For collections use an index or a method such as `set`, for example `xs[xs.len - 1] = v`. ## Failing example ```flowscope script "Last close override" data chart = subscribe(data.ohlcv) state closes = float[](100) on chart.close { if closes.len < 100 { closes.push(chart.close) } if chart.isRealtime && closes.len > 0 { closes.last() = chart.close } } ``` ## Fixed example ```flowscope script "Last close override" data chart = subscribe(data.ohlcv) state closes = float[](100) on chart.close { if closes.len < 100 { closes.push(chart.close) } if chart.isRealtime && closes.len > 0 { closes[closes.len - 1] = chart.close } } ``` ## Related - [T216](/docs/scripting/reference/diagnostics/t216) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t217 --- # T301 > type field has no type annotation **Severity** Error Every field of a user `type` needs an explicit type after a colon. Field types are never inferred, because the record layout is fixed before the script runs. Add `: Type` to the field. ## Failing example ```flowscope script "Swing levels" data chart = subscribe(data.ohlcv) type Swing { price index: int } state last: Swing? = null plot level = plot.line(title: "Last swing high", style: linestyle.step) on chart.close { let p = ta.pivotHigh(chart.high, 5, 5) if p != null { last = Swing { price: p, index: chart.index } } let s = last level.plot(s != null ? s.price : null) } ``` ## Fixed example ```flowscope script "Swing levels" data chart = subscribe(data.ohlcv) type Swing { price: float index: int } state last: Swing? = null plot level = plot.line(title: "Last swing high", style: linestyle.step) on chart.close { let p = ta.pivotHigh(chart.high, 5, 5) if p != null { last = Swing { price: p, index: chart.index } } let s = last level.plot(s != null ? s.price : null) } ``` ## Related - [T307](/docs/scripting/reference/diagnostics/t307) - [T306](/docs/scripting/reference/diagnostics/t306) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t301 --- # T302 > type contains itself **Severity** Error A record type contains a field of its own type, directly or through other records. Records are stored by value, so such a type would have infinite size. Break the cycle: store an index or an id that refers to the other record, and keep the records in an array or map. ## Failing example ```flowscope script "Level chain" data chart = subscribe(data.ohlcv) type Level { price: float next: Level } state levels = map(100) on chart.close { levels[chart.index] = chart.close ?? 0.0 } ``` ## Fixed example ```flowscope script "Level chain" data chart = subscribe(data.ohlcv) type Level { price: float nextIndex: int } state levels = map(100) on chart.close { levels[chart.index] = chart.close ?? 0.0 } ``` ## Related - [L001](/docs/scripting/reference/diagnostics/l001) - [T301](/docs/scripting/reference/diagnostics/t301) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t302 --- # T303 > type name is not UpperCamelCase **Severity** Error User type names must start with an uppercase letter and use UpperCamelCase, such as `LiqCluster` or `SwingPoint`. This keeps types visually distinct from values, which are written in lowerCamelCase. Rename the type and every place it is used. ## Failing example ```flowscope script "Liquidation clusters" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) type liq_cluster { price: float usd: float } state biggest: liq_cluster? = null on chart.close { let total = liq.total ?? 0.0 let b = biggest if b == null || total > b.usd { biggest = liq_cluster { price: chart.close ?? 0.0, usd: total } } } ``` ## Fixed example ```flowscope script "Liquidation clusters" data ( chart = subscribe(data.ohlcv) liq = subscribe(data.liquidations) ) type LiqCluster { price: float usd: float } state biggest: LiqCluster? = null on chart.close { let total = liq.total ?? 0.0 let b = biggest if b == null || total > b.usd { biggest = LiqCluster { price: chart.close ?? 0.0, usd: total } } } ``` ## Related - [T005](/docs/scripting/reference/diagnostics/t005) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t303 --- # T304 > match does not cover every case **Severity** Error A `match` used as an expression must produce a value for every possible subject, but some values are not matched by any arm. Range arms and guarded arms never count as complete coverage on their own. Add the missing cases, or end with a wildcard arm `_ => ...`. A nullable subject also needs a `null` arm or a wildcard. ## Failing example ```flowscope script "Funding label" data funding = subscribe(data.funding) on funding.close { let r = funding.rate ?? 0.0 let label = match r { v if v > 0.0 => "longs pay" v if v < 0.0 => "shorts pay" } log.info(label) } ``` ## Fixed example ```flowscope script "Funding label" data funding = subscribe(data.funding) on funding.close { let r = funding.rate ?? 0.0 let label = match r { v if v > 0.0 => "longs pay" v if v < 0.0 => "shorts pay" _ => "flat" } log.info(label) } ``` ## Related - [T305](/docs/scripting/reference/diagnostics/t305) - [P024](/docs/scripting/reference/diagnostics/p024) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t304 --- # T305 > null arm on a value that cannot be null **Severity** Warning A `match` has a `null` arm, but the subject's type is not nullable, so the arm can never run. This often happens after a value was already defaulted with `??`. Remove the `null` arm, or match on the original nullable value instead. ## Failing example ```flowscope script "OI direction" data oi = subscribe(data.oi) on oi.close { let change = (oi.close ?? 0.0) - (oi.close[1] ?? 0.0) let word = match change { null => "unknown" v if v > 0.0 => "rising" _ => "falling" } log.debug(word) } ``` ## Fixed example ```flowscope script "OI direction" data oi = subscribe(data.oi) on oi.close { let change = oi.close - oi.close[1] let word = match change { null => "unknown" v if v > 0.0 => "rising" _ => "falling" } log.debug(word) } ``` ## Related - [T304](/docs/scripting/reference/diagnostics/t304) - [N001](/docs/scripting/reference/diagnostics/n001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t305 --- # T306 > record literal has wrong fields **Severity** Error A record literal `Name { ... }` must set every field of the type exactly once, with a value of the field's type. The literal here omits a field, names a field that does not exist, repeats one, or passes a value of the wrong type. Match the declared field list. Fields cannot have defaults, so nullable fields still need an explicit `null`. ## Failing example ```flowscope script "Print log" data tape = subscribe(data.trades, minNotional: 500_000.0) type Print { price: float usd: float buy: bool } state last: Print? = null on tape.trade(tr) { let prev = last if prev != null && prev.buy != tr.isBuy { log.info(str.format("side flip at {0}", tr.price)) } last = Print { price: tr.price, usd: tr.notional } } ``` ## Fixed example ```flowscope script "Print log" data tape = subscribe(data.trades, minNotional: 500_000.0) type Print { price: float usd: float buy: bool } state last: Print? = null on tape.trade(tr) { let prev = last if prev != null && prev.buy != tr.isBuy { log.info(str.format("side flip at {0}", tr.price)) } last = Print { price: tr.price, usd: tr.notional, buy: tr.isBuy } } ``` ## Related - [T301](/docs/scripting/reference/diagnostics/t301) - [T307](/docs/scripting/reference/diagnostics/t307) - [T442](/docs/scripting/reference/diagnostics/t442) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t306 --- # T307 > duplicate field in a type **Severity** Error A `type` declaration lists the same field name twice. Rename or remove one of the fields. ## Failing example ```flowscope script "Bar snapshot" data chart = subscribe(data.ohlcv) type Snap { close: float delta: float close: float } pane p = pane(title: "Delta", height: 0.2) plot delta = plot.columns(title: "Delta", on: p) on chart.close { let snap = Snap { close: chart.close ?? 0.0, delta: (chart.buyVolume - chart.sellVolume) ?? 0.0 } delta.plot(snap.delta) } ``` ## Fixed example ```flowscope script "Bar snapshot" data chart = subscribe(data.ohlcv) type Snap { close: float delta: float } pane p = pane(title: "Delta", height: 0.2) plot delta = plot.columns(title: "Delta", on: p) on chart.close { let snap = Snap { close: chart.close ?? 0.0, delta: (chart.buyVolume - chart.sellVolume) ?? 0.0 } delta.plot(snap.delta) } ``` ## Related - [T306](/docs/scripting/reference/diagnostics/t306) - [T301](/docs/scripting/reference/diagnostics/t301) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t307 --- # T401 > function calls itself **Severity** Error A user function calls itself, either directly or through other functions. Recursion cannot be bounded before the script runs, so it is not allowed. Rewrite the computation as a `for` loop over a known range. ## Failing example ```flowscope script "Tick ladder" data chart = subscribe(data.ohlcv) fn ladder(n: int) -> float { return n <= 0 ? 0.0 : context.tickSize + ladder(n - 1) } plot above = plot.line(title: "Close + 20 ticks") on chart.close { above.plot(chart.close + ladder(20)) } ``` ## Fixed example ```flowscope script "Tick ladder" data chart = subscribe(data.ohlcv) fn ladder(n: int) -> float { return float(n) * context.tickSize } plot above = plot.line(title: "Close + 20 ticks") on chart.close { above.plot(chart.close + ladder(20)) } ``` ## Related - [L001](/docs/scripting/reference/diagnostics/l001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t401 --- # T402 > stateful calculation inside a loop **Severity** Error A `ta.*` call (or another call that keeps history between bars) appears inside a `for` loop. Every stateful call site owns one piece of history, and inside a loop a single site would be shared by all iterations, mixing their values. Move the call out of the loop. For a few variants write one call per variant; for many, compute them from a rolling buffer with collection methods. ## Failing example ```flowscope script "EMA fan" data chart = subscribe(data.ohlcv) setup lengths = [20, 50, 200] plot top = plot.line(title: "Highest EMA") state best: float = 0.0 on chart.close { best = 0.0 for len in lengths { best = math.max(best, ta.ema(chart.close, len) ?? 0.0) } top.plot(best) } ``` ## Fixed example ```flowscope script "EMA fan" data chart = subscribe(data.ohlcv) plot top = plot.line(title: "Highest EMA") on chart.close { let e20 = ta.ema(chart.close, 20) ?? 0.0 let e50 = ta.ema(chart.close, 50) ?? 0.0 let e200 = ta.ema(chart.close, 200) ?? 0.0 top.plot(math.max(e20, math.max(e50, e200))) } ``` ## Related - [T428](/docs/scripting/reference/diagnostics/t428) - [L001](/docs/scripting/reference/diagnostics/l001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t402 --- # T403 > indicator inputs come from different timeframes **Severity** Error An indicator with several series inputs (for example `ta.correlation`, `ta.crossover`) receives series from subscriptions with different timeframes. Their bars do not line up one to one, so the result would compare unrelated periods. Feed the indicator from subscriptions that share one timeframe, or compute each side on its own timeframe and compare plain values afterwards. ## Failing example ```flowscope script "Price vs OI correlation" data ( chart = subscribe(data.ohlcv) oiDaily = subscribe(data.oi, timeframe: 1D) ) pane p = pane(title: "Correlation", height: 0.2) plot corr = plot.line(title: "Close/OI correlation", on: p) on chart.close { corr.plot(ta.correlation(chart.close, oiDaily.close, 30)) } ``` ## Fixed example ```flowscope script "Price vs OI correlation" data ( chart = subscribe(data.ohlcv) oi = subscribe(data.oi) ) pane p = pane(title: "Correlation", height: 0.2) plot corr = plot.line(title: "Close/OI correlation", on: p) on chart.close { corr.plot(ta.correlation(chart.close, oi.close, 30)) } ``` ## Related - [T408](/docs/scripting/reference/diagnostics/t408) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t403 --- # T405 > event not available for this subscription **Severity** Error The handler uses an event that the subscription's source does not deliver. Bar-based sources (`ohlcv`, `oi`, `funding`, `liquidations`, `vd`, `cvd`, `flow`) have `open`, `update` and `close`; only `data.trades` has `trade(tr)`, and that is its only event. Use an event the source supports, or subscribe to the source that delivers the event you want. ## Failing example ```flowscope script "Big print marker" data chart = subscribe(data.ohlcv) plot big = plot.point(title: "Big print", color: color.poc) on chart.trade(tr) { if tr.notional > 1_000_000.0 { big.plot(tr.price) } } ``` ## Fixed example ```flowscope script "Big print marker" data tape = subscribe(data.trades, minNotional: 1_000_000.0) plot big = plot.point(title: "Big print", color: color.poc) on tape.trade(tr) { big.plot(tr.price) } ``` ## Related - [T406](/docs/scripting/reference/diagnostics/t406) - [T407](/docs/scripting/reference/diagnostics/t407) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t405 --- # T406 > duplicate handler **Severity** Error There are two handlers for the same subscription and event, for example two `on chart.close` blocks. Each subscription/event pair has at most one handler, so the execution order is never in doubt. Merge both bodies into one handler. ## Failing example ```flowscope script "Close and delta" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") pane p = pane(title: "Delta", height: 0.2) plot delta = plot.columns(title: "Delta", on: p) on chart.close { c.plot(chart.close) } on chart.close { delta.plot(chart.buyVolume - chart.sellVolume) } ``` ## Fixed example ```flowscope script "Close and delta" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") pane p = pane(title: "Delta", height: 0.2) plot delta = plot.columns(title: "Delta", on: p) on chart.close { c.plot(chart.close) delta.plot(chart.buyVolume - chart.sellVolume) } ``` ## Related - [T405](/docs/scripting/reference/diagnostics/t405) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t406 --- # T407 > handler on a snapshot source **Severity** Error `data.book`, `data.volume` and `data.hl` are snapshot sources: they do not emit events, they are queried. A handler such as `on book.update` therefore has nothing to react to. Read the snapshot from a handler of a bar or trade subscription, for example `on chart.update { let d = book.depth(0.5) }`. ## Failing example ```flowscope script "Book imbalance" data book = subscribe(data.book) pane p = pane(title: "Imbalance", height: 0.2) plot imb = plot.line(title: "Imbalance 0.5%", on: p) on book.update { let d = book.depth(0.5) if d != null { imb.plot(d.imbalance) } } ``` ## Fixed example ```flowscope script "Book imbalance" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) pane p = pane(title: "Imbalance", height: 0.2) plot imb = plot.line(title: "Imbalance 0.5%", on: p) on chart.update { let d = book.depth(0.5) if d != null { imb.plot(d.imbalance) } } ``` ## Related - [T405](/docs/scripting/reference/diagnostics/t405) - [T422](/docs/scripting/reference/diagnostics/t422) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t407 --- # T408 > indicator on a calendar timeframe **Severity** Error A `ta.*` indicator receives a series from a subscription on a calendar timeframe (`1M`). Months have different lengths, so period-based indicators would weigh them inconsistently. Run the indicator on a fixed-length timeframe (seconds to weeks), or read the monthly values directly without an indicator. ## Failing example ```flowscope script "Monthly EMA" data monthly = subscribe(data.ohlcv, timeframe: 1M) plot ema = plot.line(title: "Monthly EMA 12", style: linestyle.step) on monthly.close { ema.plot(ta.ema(monthly.close, 12)) } ``` ## Fixed example ```flowscope script "Monthly EMA" data weekly = subscribe(data.ohlcv, timeframe: 1W) plot ema = plot.line(title: "Weekly EMA 52", style: linestyle.step) on weekly.close { ema.plot(ta.ema(weekly.close, 52)) } ``` ## Related - [T111](/docs/scripting/reference/diagnostics/t111) - [T403](/docs/scripting/reference/diagnostics/t403) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t408 --- # T413 > indicator length must be known before the script runs **Severity** Error The length (window) argument of a `ta.*` call is computed while the script runs, for example from `state` or bar data. Indicator memory is reserved once at start, so the length must be fixed by then. Use a literal, a setup value or an `input`. To vary the effective window at run time, keep values in a rolling buffer and aggregate it yourself. ## Failing example ```flowscope script "Adaptive SMA" data chart = subscribe(data.ohlcv) state len: int = 20 plot sma = plot.line(title: "Adaptive SMA") on chart.close { len = chart.isRealtime ? 10 : 20 sma.plot(ta.sma(chart.close, len)) } ``` ## Fixed example ```flowscope script "Adaptive SMA" input len = input.int(20, title: "Length", min: 2, max: 500) data chart = subscribe(data.ohlcv) plot sma = plot.line(title: "SMA") on chart.close { sma.plot(ta.sma(chart.close, len)) } ``` ## Related - [S005](/docs/scripting/reference/diagnostics/s005) - [T414](/docs/scripting/reference/diagnostics/t414) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t413 --- # T414 > indicator length out of range **Severity** Error A constant indicator length lies outside the range the function accepts. Most windows must be at least 1 (some, like `ta.stdev` or `ta.correlation`, need 2) and at most the maximum history depth. Choose a value in the range named in the message. For inputs, set `min:` and `max:` so the user cannot pick an invalid length. ## Failing example ```flowscope script "Zero EMA" data chart = subscribe(data.ohlcv) plot ema = plot.line(title: "EMA") on chart.close { ema.plot(ta.ema(chart.close, 0)) } ``` ## Fixed example ```flowscope script "Zero EMA" data chart = subscribe(data.ohlcv) plot ema = plot.line(title: "EMA") on chart.close { ema.plot(ta.ema(chart.close, 9)) } ``` ## Related - [T413](/docs/scripting/reference/diagnostics/t413) - [L007](/docs/scripting/reference/diagnostics/l007) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t414 --- # T415 > plot method does not match the panel **Severity** Error Plots on time-axis panes and panels are fed with `p.plot(value)`, one value per bar. Panels with `axis: axis.category` or `axis: axis.index` are filled with `p.set(key, value)`. The call here uses the method of the other kind. Use `plot` for time-based panes, and `set` for category or index panels, or move the plot to a panel of the right kind. ## Failing example ```flowscope script "Close line" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { c.set("close", chart.close) } ``` ## Fixed example ```flowscope script "Close line" data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { c.plot(chart.close) } ``` ## Related - [T215](/docs/scripting/reference/diagnostics/t215) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t415 --- # T416 > invalid str.format template **Severity** Error A constant template passed to `str.format` has unbalanced braces, a placeholder index without a matching argument, or an unknown format spec. Placeholders are `{0}`, `{1:.2}` and so on; literal braces are written `{{` and `}}`. Fix the template so every placeholder is closed and refers to an argument that exists. ## Failing example ```flowscope script "Funding text" data funding = subscribe(data.funding) on funding.close { log.info(str.format("rate {0:.4} / predicted {1", funding.rate ?? 0.0, funding.predicted ?? 0.0)) } ``` ## Fixed example ```flowscope script "Funding text" data funding = subscribe(data.funding) on funding.close { log.info(str.format("rate {0:.4} / predicted {1:.4}", funding.rate ?? 0.0, funding.predicted ?? 0.0)) } ``` ## Related - [P007](/docs/scripting/reference/diagnostics/p007) - [T114](/docs/scripting/reference/diagnostics/t114) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t416 --- # T420 > trade fields are read-only **Severity** Error The `tr` value of a trade handler describes a trade that already happened; its fields cannot be assigned. Copy the value into a local (`let adjusted = tr.size * 2.0`) or into your own record and change that. ## Failing example ```flowscope script "Signed size" data tape = subscribe(data.trades, minNotional: 100_000.0) state net: float = 0.0 on tape.trade(tr) { if !tr.isBuy { tr.size = -tr.size } net += tr.size } ``` ## Fixed example ```flowscope script "Signed size" data tape = subscribe(data.trades, minNotional: 100_000.0) state net: float = 0.0 on tape.trade(tr) { let signed = tr.isBuy ? tr.size : -tr.size net += signed } ``` ## Related - [T216](/docs/scripting/reference/diagnostics/t216) - [C006](/docs/scripting/reference/diagnostics/c006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t420 --- # T422 > book or profile view kept beyond the current call **Severity** Error Results of `book.depth`, `book.level` and `volume.profile` are views into data that the engine overwrites on the next update. They may be read within the handler call that obtained them, but not stored in `state`, in collections or in records, or returned from functions. Extract the numbers you need (for example `d.imbalance` or `prof.poc()`) and store those instead. ## Failing example ```flowscope script "Depth memory" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) state lastDepth: BookDepth? = null pane p = pane(title: "Imbalance", height: 0.2) plot imb = plot.line(title: "Imbalance 1%", on: p) on chart.update { lastDepth = book.depth(1.0) let d = lastDepth imb.plot(d != null ? d.imbalance : null) } ``` ## Fixed example ```flowscope script "Depth memory" data ( chart = subscribe(data.ohlcv) book = subscribe(data.book) ) state lastImbalance: float? = null pane p = pane(title: "Imbalance", height: 0.2) plot imb = plot.line(title: "Imbalance 1%", on: p) on chart.update { let d = book.depth(1.0) if d != null { lastImbalance = d.imbalance } imb.plot(lastImbalance) } ``` ## Related - [T407](/docs/scripting/reference/diagnostics/t407) - [C003](/docs/scripting/reference/diagnostics/c003) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t422 --- # T425 > fill has both a solid color and a gradient **Severity** Error `fill.between` paints either one solid color (`color:`) or a gradient (`gradient:`). Passing both leaves it unclear which one should win. Keep one of the two arguments. ## Failing example ```flowscope script "Band fill" data chart = subscribe(data.ohlcv) setup shade = color.scale(positions: [0.0, 1.0], colors: [color.withAlpha(color.accent, 10), color.withAlpha(color.accent, 60)]) plot upper = plot.line(title: "Upper") plot lower = plot.line(title: "Lower") fill band = fill.between(upper, lower, color: color.withAlpha(color.accent, 30), gradient: shade) on chart.close { let (_, up, lo) = ta.bb(chart.close, 20, 2.0) upper.plot(up) lower.plot(lo) } ``` ## Fixed example ```flowscope script "Band fill" data chart = subscribe(data.ohlcv) setup shade = color.scale(positions: [0.0, 1.0], colors: [color.withAlpha(color.accent, 10), color.withAlpha(color.accent, 60)]) plot upper = plot.line(title: "Upper") plot lower = plot.line(title: "Lower") fill band = fill.between(upper, lower, gradient: shade) on chart.close { let (_, up, lo) = ta.bb(chart.close, 20, 2.0) upper.plot(up) lower.plot(lo) } ``` ## Related - [S013](/docs/scripting/reference/diagnostics/s013) - [T203](/docs/scripting/reference/diagnostics/t203) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t425 --- # T426 > lambda in an invalid position or with a block body **Severity** Error Lambdas exist only as the function argument of collection methods such as `map`, `filter`, `reduce`, `any`, `all` and `count`. Their body is a single expression without braces. A lambda stored in a variable, passed elsewhere, or written with a `{ }` body is rejected. Write the lambda inline in the collection call with an expression body. For reusable logic, declare a named `fn` and call it from the lambda. ## Failing example ```flowscope script "Notional in thousands" data tape = subscribe(data.trades, minNotional: 25_000.0) state recent = rolling(50) on tape.trade(tr) { recent.push(tr.notional) let inK = recent.map(v => { v / 1000.0 }) log.debug(str.format("max {0:.1}k", inK.max() ?? 0.0)) } ``` ## Fixed example ```flowscope script "Notional in thousands" data tape = subscribe(data.trades, minNotional: 25_000.0) state recent = rolling(50) on tape.trade(tr) { recent.push(tr.notional) let inK = recent.map(v => v / 1000.0) log.debug(str.format("max {0:.1}k", inK.max() ?? 0.0)) } ``` ## Related - [T427](/docs/scripting/reference/diagnostics/t427) - [T430](/docs/scripting/reference/diagnostics/t430) - [S018](/docs/scripting/reference/diagnostics/s018) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t426 --- # T427 > invalid lambda parameter list **Severity** Error The lambda has the wrong number of parameters for the method it is passed to. `map`, `filter`, `any`, `all` and `count` take a one-parameter lambda (`v => ...`); `reduce` takes an accumulator and an element (`(acc, v) => ...`) with two different names. Adjust the parameter list to the method. ## Failing example ```flowscope script "Sum of large prints" data tape = subscribe(data.trades, minNotional: 100_000.0) state recent = rolling(20) on tape.trade(tr) { recent.push(tr.notional) let total = recent.reduce(0.0, v => v + v) log.debug(str.formatVolume(total)) } ``` ## Fixed example ```flowscope script "Sum of large prints" data tape = subscribe(data.trades, minNotional: 100_000.0) state recent = rolling(20) on tape.trade(tr) { recent.push(tr.notional) let total = recent.reduce(0.0, (acc, v) => acc + v) log.debug(str.formatVolume(total)) } ``` ## Related - [T426](/docs/scripting/reference/diagnostics/t426) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t427 --- # T428 > stateful call, randomness or drawing inside a lambda **Severity** Error Lambdas must be pure: they run once per element, a different number of times on each call, so they cannot call `ta.*`, `math.random`, `draw.*`, plots, logs, alerts or strategy orders, not even indirectly through a user function. Compute only plain values inside the lambda, and do the stateful work, drawing or output in the handler body. ## Failing example ```flowscope script "Level lines" data chart = subscribe(data.ohlcv) setup levels = [60_000.0, 65_000.0, 70_000.0] on chart.close { if chart.isFirst { let keys = levels.map(p => draw.hline(str.formatNumber(p, 0), p, color: color.muted)) log.debug(str.format("{0} levels", keys.len)) } } ``` ## Fixed example ```flowscope script "Level lines" data chart = subscribe(data.ohlcv) setup levels = [60_000.0, 65_000.0, 70_000.0] on chart.close { if chart.isFirst { for p in levels { draw.hline(str.formatNumber(p, 0), p, color: color.muted) } } } ``` ## Related - [T402](/docs/scripting/reference/diagnostics/t402) - [T426](/docs/scripting/reference/diagnostics/t426) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t428 --- # T430 > collection transform must be its own statement **Severity** Error `map`, `filter` and `reduce` may only appear as the whole right-hand side of a `let`, an assignment, or as a statement of their own. Nesting them inside a larger expression or condition is not allowed, so the cost of each transform stays visible in the code. Bind the transform to a local first and use that local in the larger expression. ## Failing example ```flowscope script "Whale count" data tape = subscribe(data.trades, minNotional: 50_000.0) state recent = rolling(100) alert whales = alert(title: "Three whales in the last 100 prints") on tape.trade(tr) { recent.push(tr.notional) if recent.filter(v => v >= 1_000_000.0).len >= 3 { whales.trigger() } } ``` ## Fixed example ```flowscope script "Whale count" data tape = subscribe(data.trades, minNotional: 50_000.0) state recent = rolling(100) alert whales = alert(title: "Three whales in the last 100 prints") on tape.trade(tr) { recent.push(tr.notional) let big = recent.filter(v => v >= 1_000_000.0) if big.len >= 3 { whales.trigger() } } ``` ## Related - [T426](/docs/scripting/reference/diagnostics/t426) - [S018](/docs/scripting/reference/diagnostics/s018) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t430 --- # T442 > record literal without a type name **Severity** Error A brace block with `field: value` pairs was written without a type name in front of it. Flowscope Script has no anonymous records; each record literal names its type, so the compiler knows its fields. Declare a `type` and put its name before the braces: `Zone { top: h, bottom: l }`. ## Failing example ```flowscope script "Last zone" data chart = subscribe(data.ohlcv) type Zone { top: float bottom: float } state zone: Zone? = null plot mid = plot.line(title: "Zone midpoint") on chart.close { let z = zone mid.plot(z != null ? (z.top + z.bottom) / 2.0 : null) zone = { top: chart.high ?? 0.0, bottom: chart.low ?? 0.0 } } ``` ## Fixed example ```flowscope script "Last zone" data chart = subscribe(data.ohlcv) type Zone { top: float bottom: float } state zone: Zone? = null plot mid = plot.line(title: "Zone midpoint") on chart.close { let z = zone mid.plot(z != null ? (z.top + z.bottom) / 2.0 : null) zone = Zone { top: chart.high ?? 0.0, bottom: chart.low ?? 0.0 } } ``` ## Related - [T306](/docs/scripting/reference/diagnostics/t306) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t442 --- # T449 > isLast read in a close handler **Severity** Warning `x.isLast` is true only while an update handler runs at the live edge. A close handler always works on a confirmed bar, so `isLast` is false there on every call and any code behind it never runs. Move the check into `on x.update`. If you only want to know whether the confirmed bar was received live, use `x.isRealtime` instead. ## Failing example ```flowscope script "Live label" data chart = subscribe(data.ohlcv) on chart.close { if chart.isLast { draw.label("last", chart.time ?? time.now(), chart.close ?? 0.0, "live", color: color.white, bg: color.accent) } } ``` ## Fixed example ```flowscope script "Live label" data chart = subscribe(data.ohlcv) on chart.update { if chart.isLast { draw.label("last", chart.time ?? time.now(), chart.close ?? 0.0, "live", color: color.white, bg: color.accent) } } ``` ## Related - [T405](/docs/scripting/reference/diagnostics/t405) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/t449 --- # N001 > value may be null here **Severity** Error A nullable value (`T?`) is used where a non-null value is required: as a condition, as an argument to a parameter of type `T`, as a collection element that cannot be null, or assigned to a non-nullable variable. Market fields are nullable because a bar can be missing, so this is the most common diagnostic in new scripts. Check the value first (`if x != null { ... }`, after which `x` counts as non-null inside the block), or supply a fallback with `x ?? fallback`. Conditions can use `?? false`. ## Failing example ```flowscope script "Close in ticks" data chart = subscribe(data.ohlcv) fn toTicks(price: float) -> float { return price / context.tickSize } pane p = pane(title: "Ticks", height: 0.2) plot t = plot.line(title: "Close (ticks)", on: p) on chart.close { t.plot(toTicks(chart.close)) } ``` ## Fixed example ```flowscope script "Close in ticks" data chart = subscribe(data.ohlcv) fn toTicks(price: float) -> float { return price / context.tickSize } pane p = pane(title: "Ticks", height: 0.2) plot t = plot.line(title: "Close (ticks)", on: p) on chart.close { let c = chart.close if c != null { t.plot(toTicks(c)) } } ``` ## Related - [N002](/docs/scripting/reference/diagnostics/n002) - [N003](/docs/scripting/reference/diagnostics/n003) - [P009](/docs/scripting/reference/diagnostics/p009) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/n001 --- # N002 > null check on a field does not narrow it **Severity** Error A field or element (such as `chart.close`, `s.price` or `xs[0]`) was checked against null, and then used as if it were non-null. Narrowing only applies to local bindings: a field can be read again from a different place, so the check does not carry over to the next read. Copy the value into a `let` local, check the local, and use the local afterwards. ## Failing example ```flowscope script "OI change" data oi = subscribe(data.oi) fn pct(now: float, before: float) -> float { return (now - before) / before * 100.0 } pane p = pane(title: "OI change %", height: 0.2) plot ch = plot.columns(title: "OI change", on: p) on oi.close { if oi.close != null && oi.close[1] != null { ch.plot(pct(oi.close, oi.close[1])) } } ``` ## Fixed example ```flowscope script "OI change" data oi = subscribe(data.oi) fn pct(now: float, before: float) -> float { return (now - before) / before * 100.0 } pane p = pane(title: "OI change %", height: 0.2) plot ch = plot.columns(title: "OI change", on: p) on oi.close { let now = oi.close let before = oi.close[1] if now != null && before != null { ch.plot(pct(now, before)) } } ``` ## Related - [N001](/docs/scripting/reference/diagnostics/n001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/n002 --- # N003 > null needs an explicit type **Severity** Error A bare `null` was used where no type can be inferred from context, for example `state peak = null` or `let x = null`. The compiler cannot tell whether it is meant to be a `float?`, a `time?` or something else. Add a type annotation (`state peak: float? = null`), or use the `null` where the expected type is known, such as a typed parameter or the other branch of a ternary. ## Failing example ```flowscope script "Swing memory" data chart = subscribe(data.ohlcv) state lastPivot = null plot level = plot.line(title: "Last pivot high", style: linestyle.step) on chart.close { let p = ta.pivotHigh(chart.high, 3, 3) if p != null { lastPivot = p } level.plot(lastPivot) } ``` ## Fixed example ```flowscope script "Swing memory" data chart = subscribe(data.ohlcv) state lastPivot: float? = null plot level = plot.line(title: "Last pivot high", style: linestyle.step) on chart.close { let p = ta.pivotHigh(chart.high, 3, 3) if p != null { lastPivot = p } level.plot(lastPivot) } ``` ## Related - [N001](/docs/scripting/reference/diagnostics/n001) - [T208](/docs/scripting/reference/diagnostics/t208) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/n003 --- # C001 > setup collection is read-only **Severity** Error Arrays and maps created in `setup` (including literals such as `[1.0, 2.0]` and `{"binance": 1.0}`) are frozen once setup finishes. Calling `push`, `set`, `remove`, `clear` or assigning an element changes them, which is not allowed. Keep the fixed data in `setup`, and declare a `state` collection for anything that changes while the script runs. ## Failing example ```flowscope script "Touched levels" data chart = subscribe(data.ohlcv) setup levels = [60_000.0, 65_000.0, 70_000.0] on chart.close { let h = chart.high ?? 0.0 let l = chart.low ?? 0.0 if h >= 75_000.0 && l <= 75_000.0 { levels.push(75_000.0) } } ``` ## Fixed example ```flowscope script "Touched levels" data chart = subscribe(data.ohlcv) setup levels = [60_000.0, 65_000.0, 70_000.0] state touched = float[](16) on chart.close { let h = chart.high ?? 0.0 let l = chart.low ?? 0.0 for lvl in levels { if h >= lvl && l <= lvl && !touched.contains(lvl) && touched.len < 16 { touched.push(lvl) } } } ``` ## Related - [C006](/docs/scripting/reference/diagnostics/c006) - [T216](/docs/scripting/reference/diagnostics/t216) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/c001 --- # C003 > collection cannot contain another collection **Severity** Error A collection created at run time (in `state` or a handler) has another collection as its element or value type, such as `map`. Run-time collections have a fixed capacity per element type, and nesting them would make their memory unbounded. Flatten the data: use composite keys (`"binance:longs"`), a record type with plain fields, or one collection per inner list. ## Failing example ```flowscope script "Per-venue history" data chart = subscribe(data.ohlcv) state history = map(8) on chart.close { log.debug(str.format("{0} venues", history.len)) } ``` ## Fixed example ```flowscope script "Per-venue history" data chart = subscribe(data.ohlcv) state history = map(64) on chart.close { history[str.format("{0}:{1}", context.exchange, chart.index)] = chart.close ?? 0.0 log.debug(str.format("{0} entries", history.len)) } ``` ## Related - [T106](/docs/scripting/reference/diagnostics/t106) - [L007](/docs/scripting/reference/diagnostics/l007) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/c003 --- # C004 > key is missing from a setup map **Severity** Error A setup map literal is frozen, so its set of keys is known before the script runs. This lookup uses a constant key that is not in the map and would always return null. Use one of the keys present in the literal (check spelling and case), or add the key to the literal. ## Failing example ```flowscope script "Venue weights" data cvd = subscribe(data.cvd, exchange: "okx", bucket: "day") setup weights = {"binance-futures": 1.0, "bybit": 0.8} pane p = pane(title: "Weighted CVD", height: 0.25) plot line = plot.line(title: "OKX CVD x weight", on: p) on cvd.close { line.plot(cvd.close * (weights["okx"] ?? 1.0)) } ``` ## Fixed example ```flowscope script "Venue weights" data cvd = subscribe(data.cvd, exchange: "okx", bucket: "day") setup weights = {"binance-futures": 1.0, "bybit": 0.8, "okx": 0.6} pane p = pane(title: "Weighted CVD", height: 0.25) plot line = plot.line(title: "OKX CVD x weight", on: p) on cvd.close { line.plot(cvd.close * (weights["okx"] ?? 1.0)) } ``` ## Related - [C001](/docs/scripting/reference/diagnostics/c001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/c004 --- # C005 > empty array needs a capacity **Severity** Error An empty literal `[]` gives neither an element type nor a capacity, so the compiler cannot reserve storage for it. Use a typed constructor with a capacity, such as `float[](50)`, `rolling(50)` or `map(16)`. ## Failing example ```flowscope script "Swing lows" data chart = subscribe(data.ohlcv) state lows = [] on chart.close { let p = ta.pivotLow(chart.low, 5, 5) if p != null && lows.len < 50 { lows.push(p) } } ``` ## Fixed example ```flowscope script "Swing lows" data chart = subscribe(data.ohlcv) state lows = float[](50) on chart.close { let p = ta.pivotLow(chart.low, 5, 5) if p != null && lows.len < 50 { lows.push(p) } } ``` ## Related - [L007](/docs/scripting/reference/diagnostics/l007) - [L008](/docs/scripting/reference/diagnostics/l008) - [N003](/docs/scripting/reference/diagnostics/n003) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/c005 --- # C006 > subscription series is read-only **Severity** Error A series that belongs to a subscription (`chart.close`, `oi.value`, `funding.rate` and so on) is filled by the engine; scripts can read it with `[n]` but cannot push to it or overwrite its elements, even through another name. Keep your own derived history in a `state` rolling buffer and push into that. ## Failing example ```flowscope script "Smoothed closes" data chart = subscribe(data.ohlcv) plot avg = plot.line(title: "Mean of last 10") on chart.close { chart.close.push(ta.ema(chart.close, 3)) avg.plot(ta.sma(chart.close, 10)) } ``` ## Fixed example ```flowscope script "Smoothed closes" data chart = subscribe(data.ohlcv) state smooth = rolling(10) plot avg = plot.line(title: "Mean of last 10") on chart.close { smooth.push(ta.ema(chart.close, 3)) avg.plot(smooth.avg()) } ``` ## Related - [C001](/docs/scripting/reference/diagnostics/c001) - [T420](/docs/scripting/reference/diagnostics/t420) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/c006 --- # L001 > too many stateful call sites **Severity** Error The script contains more stateful call sites (`ta.*` calls and other calls that keep history) than one script may use. Each site reserves its own history buffer, and the total is capped to keep memory and the per-batch step budget predictable. The message shows the count and the limit. Remove indicators you do not plot, share one result between several uses by storing it in a `let`, or split the work into several scripts. ## Failing example ```flowscope script "Everything at once" data chart = subscribe(data.ohlcv) plot e1 = plot.line(title: "EMA 1") on chart.close { // imagine several hundred more distinct ta.* calls below e1.plot(ta.ema(chart.close, 1)) log.debug(str.format("{0}", ta.ema(chart.close, 2) ?? 0.0)) log.debug(str.format("{0}", ta.ema(chart.close, 3) ?? 0.0)) } ``` ## Fixed example ```flowscope script "Everything at once" data chart = subscribe(data.ohlcv) plot e1 = plot.line(title: "EMA 21") on chart.close { e1.plot(ta.ema(chart.close, 21)) } ``` ## Related - [T402](/docs/scripting/reference/diagnostics/t402) - [L006](/docs/scripting/reference/diagnostics/l006) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l001 --- # L006 > too many subscriptions **Severity** Error After identical subscriptions are merged, the script still requests more distinct data streams than one script may open. Every stream costs history download, memory and a live connection share. Reuse subscriptions (two `subscribe` calls with identical arguments count once), drop the ones you do not need, or use the aggregated market `"all"` instead of one subscription per venue. ## Failing example ```flowscope script "Venue sweep" data ( // imagine many more venue/timeframe combinations here b1 = subscribe(data.ohlcv, exchange: "binance-futures", timeframe: 1m) y1 = subscribe(data.ohlcv, exchange: "bybit", timeframe: 1m) o1 = subscribe(data.ohlcv, exchange: "okx", timeframe: 1m) ) plot c = plot.line(title: "Summed volume") on b1.close { c.plot((b1.volume ?? 0.0) + (y1.volume ?? 0.0) + (o1.volume ?? 0.0)) } ``` ## Fixed example ```flowscope script "Venue sweep" data agg = subscribe(data.ohlcv, exchange: "all", timeframe: 1m) plot c = plot.line(title: "Summed volume") on agg.close { c.plot(agg.volume) } ``` ## Related - [T014](/docs/scripting/reference/diagnostics/t014) - [S012](/docs/scripting/reference/diagnostics/s012) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l006 --- # L007 > capacity exceeds the supported limit **Severity** Error An array, rolling buffer or map asks for more elements than a single collection may hold, or a subscription requests more history than can be kept. The message names the maximum. Choose a smaller capacity. If you need long statistics, aggregate as you go (running sums, `ta.cum`) instead of storing every value. ## Failing example ```flowscope script "All the closes" data chart = subscribe(data.ohlcv) state closes = rolling(100_000_000) plot avg = plot.line(title: "Mean close") on chart.close { closes.push(chart.close) avg.plot(closes.avg()) } ``` ## Fixed example ```flowscope script "All the closes" data chart = subscribe(data.ohlcv) state total: float = 0.0 state n: int = 0 plot avg = plot.line(title: "Mean close") on chart.close { let c = chart.close if c != null { total += c n += 1 } avg.plot(n > 0 ? total / float(n) : null) } ``` ## Related - [L008](/docs/scripting/reference/diagnostics/l008) - [S005](/docs/scripting/reference/diagnostics/s005) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l007 --- # L008 > negative capacity **Severity** Error A collection constructor received a negative capacity, which has no meaning. Use a positive capacity. If the value comes from an input, give the input a `min:` of 1. ## Failing example ```flowscope script "Recent highs" data chart = subscribe(data.ohlcv) state highs = rolling(-20) plot top = plot.line(title: "Highest of buffer") on chart.close { highs.push(chart.high) top.plot(highs.max()) } ``` ## Fixed example ```flowscope script "Recent highs" data chart = subscribe(data.ohlcv) state highs = rolling(20) plot top = plot.line(title: "Highest of buffer") on chart.close { highs.push(chart.high) top.plot(highs.max()) } ``` ## Related - [L007](/docs/scripting/reference/diagnostics/l007) - [S005](/docs/scripting/reference/diagnostics/s005) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l008 --- # L010 > too many inputs **Severity** Error The script declares more inputs than the settings dialog of one script supports. Layout rows (`input.group`, `input.section`, `input.tab`) do not count. Combine related settings (for example one `input.select` instead of several booleans), or remove inputs you never change. ## Failing example ```flowscope script "Knob farm" input ( // imagine several hundred more inputs here len1 = input.int(10, title: "Length 1") len2 = input.int(20, title: "Length 2") ) data chart = subscribe(data.ohlcv) plot a = plot.line(title: "SMA") on chart.close { a.plot(ta.sma(chart.close, len1 + len2)) } ``` ## Fixed example ```flowscope script "Knob farm" input len = input.int(30, title: "Length") data chart = subscribe(data.ohlcv) plot a = plot.line(title: "SMA") on chart.close { a.plot(ta.sma(chart.close, len)) } ``` ## Related - [T016](/docs/scripting/reference/diagnostics/t016) - [L011](/docs/scripting/reference/diagnostics/l011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l010 --- # L011 > too many plots **Severity** Error The script declares more plots than one script can render. Plot families created with `setup ... = for` count every member. Merge plots that show related values (for example one histogram with per-bar colors instead of two), use `draw.*` entities for sparse annotations, or move part of the output into a second script. ## Failing example ```flowscope script "Plot farm" data chart = subscribe(data.ohlcv) plot ( // imagine several hundred more plot declarations here p1 = plot.line(title: "Line 1") p2 = plot.line(title: "Line 2") ) on chart.close { p1.plot(chart.high) p2.plot(chart.low) } ``` ## Fixed example ```flowscope script "Plot farm" data chart = subscribe(data.ohlcv) plot ( hi = plot.line(title: "High") lo = plot.line(title: "Low") ) on chart.close { hi.plot(chart.high) lo.plot(chart.low) } ``` ## Related - [L010](/docs/scripting/reference/diagnostics/l010) - [L013](/docs/scripting/reference/diagnostics/l013) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l011 --- # L013 > too many alerts **Severity** Error The script declares more alerts than one script may expose. Each alert is a separate entry in the alert dialog. Use one alert with a payload that carries the details (level, side, size) instead of one alert per case. ## Failing example ```flowscope script "Level alerts" data chart = subscribe(data.ohlcv) alert ( // imagine dozens more alerts, one per price level at60 = alert(title: "Crossed 60k", onClose: true) at65 = alert(title: "Crossed 65k", onClose: true) ) on chart.close { if ta.cross(chart.close, 60_000.0) { at60.trigger() } if ta.cross(chart.close, 65_000.0) { at65.trigger() } } ``` ## Fixed example ```flowscope script "Level alerts" data chart = subscribe(data.ohlcv) type LevelHit { level: float } setup levels = [60_000.0, 65_000.0] alert crossed = alert(title: "Crossed a level", onClose: true, payload: LevelHit) on chart.close { let c = chart.close let prev = chart.close[1] if c != null && prev != null { for lvl in levels { if (prev - lvl) * (c - lvl) < 0.0 { crossed.trigger(LevelHit { level: lvl }) } } } } ``` ## Related - [S020](/docs/scripting/reference/diagnostics/s020) - [L011](/docs/scripting/reference/diagnostics/l011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l013 --- # L015 > expressions or blocks nest too deeply **Severity** Error Expressions and blocks may nest at most 128 levels: parentheses inside parentheses, a very long chain like `a + b + c + …`, `if` inside `if`, or a long `else if` ladder. Scripts are shared between traders, so the compiler refuses source that would exhaust the stack of the computer compiling it. Real scripts stay far below the limit; generated code is the usual cause. After this error the rest of the file is not checked. Split the expression into `let` steps, move parts into functions, or turn an `else if` ladder into a `match`. ## Failing example ```flowscope script "Too deep" data chart = subscribe(data.ohlcv) plot p = plot.line(title: "P") on chart.close { p.plot(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((chart.close ?? 0.0))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) } ``` ## Fixed example ```flowscope script "Too deep" data chart = subscribe(data.ohlcv) plot p = plot.line(title: "P") on chart.close { let close = chart.close ?? 0.0 p.plot(close) } ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/l015 --- # B001 > strategy order outside a handler **Severity** Error `strategy.entry`, `strategy.order`, `strategy.exit`, `strategy.close`, `strategy.closeAll`, `strategy.cancel` and `strategy.cancelAll` place or change orders in the broker emulator. Orders belong to a specific bar, so they can only be issued from handler code (directly or through a user function called from a handler), not from `setup`, `state` initializers or input defaults. Move the call into a handler. To open a position at the very start of the data, guard it with `chart.isFirst`. ## Failing example ```flowscope script "Buy and hold" strategy defaultQty = qty.percentOfEquity(100.0) data chart = subscribe(data.ohlcv) setup opening = strategy.entry("Hold", direction.long) plot eq = plot.line(title: "Equity") on chart.close { eq.plot(strategy.equity) } ``` ## Fixed example ```flowscope script "Buy and hold" strategy defaultQty = qty.percentOfEquity(100.0) data chart = subscribe(data.ohlcv) plot eq = plot.line(title: "Equity") on chart.close { if chart.isFirst { strategy.entry("Hold", direction.long) } eq.plot(strategy.equity) } ``` ## Related - [B002](/docs/scripting/reference/diagnostics/b002) - [B013](/docs/scripting/reference/diagnostics/b013) - [S002](/docs/scripting/reference/diagnostics/s002) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b001 --- # B002 > strategy function without a strategy declaration **Severity** Error The script calls a `strategy.*` order function or reads `strategy.position`, `strategy.equity` or another strategy value, but it has no `strategy` declaration. Without the declaration the script is an indicator: there is no broker emulator, no capital and no Strategy Tester. Add a `strategy` declaration after the `script` line, either the single-line form (`strategy initialCapital = 10_000.0`) or the grouped form with all the properties you need. ## Failing example ```flowscope script "EMA cross" data chart = subscribe(data.ohlcv) on chart.close { let fast = ta.ema(chart.close, 9) let slow = ta.ema(chart.close, 21) if ta.crossover(fast, slow) { strategy.entry("Long", direction.long) } if ta.crossunder(fast, slow) { strategy.close("Long") } } ``` ## Fixed example ```flowscope script "EMA cross" strategy initialCapital = 10_000.0 data chart = subscribe(data.ohlcv) on chart.close { let fast = ta.ema(chart.close, 9) let slow = ta.ema(chart.close, 21) if ta.crossover(fast, slow) { strategy.entry("Long", direction.long) } if ta.crossunder(fast, slow) { strategy.close("Long") } } ``` ## Related - [B003](/docs/scripting/reference/diagnostics/b003) - [B001](/docs/scripting/reference/diagnostics/b001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b002 --- # B003 > more than one strategy declaration **Severity** Error A script can be a strategy only once: it has one broker emulator, one equity curve and one set of properties. A second `strategy` declaration was found. Merge both into a single grouped declaration: `strategy ( ... )` with one property per line. ## Failing example ```flowscope script "Funding fade" strategy initialCapital = 25_000.0 strategy leverage = 2.0 data ( chart = subscribe(data.ohlcv) funding = subscribe(data.funding) ) on chart.close { let r = funding.rate ?? 0.0 if r > 0.0005 && strategy.position.size == 0.0 { strategy.entry("Fade", direction.short) } if r < 0.0001 { strategy.close("Fade") } } ``` ## Fixed example ```flowscope script "Funding fade" strategy ( initialCapital = 25_000.0 leverage = 2.0 ) data ( chart = subscribe(data.ohlcv) funding = subscribe(data.funding) ) on chart.close { let r = funding.rate ?? 0.0 if r > 0.0005 && strategy.position.size == 0.0 { strategy.entry("Fade", direction.short) } if r < 0.0001 { strategy.close("Fade") } } ``` ## Related - [B004](/docs/scripting/reference/diagnostics/b004) - [B002](/docs/scripting/reference/diagnostics/b002) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b003 --- # B004 > strategy property set twice **Severity** Error The same property appears twice in the `strategy` declaration. Only one value can be in effect, and the Strategy Tester's Properties tab has to show which one. Remove one of the lines. ## Failing example ```flowscope script "Breakout" strategy ( initialCapital = 10_000.0 commission = commission.percent(0.04) commission = commission.percent(0.055) ) data chart = subscribe(data.ohlcv) state prevHigh: float? = null on chart.close { if chart.close > prevHigh { strategy.entry("Break", direction.long) } prevHigh = ta.highest(chart.high, 20) } ``` ## Fixed example ```flowscope script "Breakout" strategy ( initialCapital = 10_000.0 commission = commission.percent(0.055) ) data chart = subscribe(data.ohlcv) state prevHigh: float? = null on chart.close { if chart.close > prevHigh { strategy.entry("Break", direction.long) } prevHigh = ta.highest(chart.high, 20) } ``` ## Related - [B003](/docs/scripting/reference/diagnostics/b003) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b004 --- # B005 > exit refers to an unknown entry id **Severity** Warning `strategy.exit(..., from: "id")` attaches a bracket to the entry with that id. The script never calls `strategy.entry` or `strategy.order` with this constant id, so the exit can never become active. Ids are compared exactly, including case. Use the id of an existing entry, or leave `from:` out to attach the exit to every open entry. ## Failing example ```flowscope script "Bracketed long" strategy defaultQty = qty.percentOfEquity(20.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.vwap(chart)) { strategy.entry("Long", direction.long) } strategy.exit("Bracket", from: "long", profit: 400.0, loss: 200.0) } ``` ## Fixed example ```flowscope script "Bracketed long" strategy defaultQty = qty.percentOfEquity(20.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.vwap(chart)) { strategy.entry("Long", direction.long) } strategy.exit("Bracket", from: "Long", profit: 400.0, loss: 200.0) } ``` ## Related - [B009](/docs/scripting/reference/diagnostics/b009) - [B010](/docs/scripting/reference/diagnostics/b010) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b005 --- # B006 > pyramiding is negative **Severity** Error `pyramiding` is the number of additional entries allowed in the same direction while a position is open. `0` means a single entry; a negative value is meaningless. Use `0` or a positive integer. ## Failing example ```flowscope script "Scale in" strategy ( defaultQty = qty.percentOfEquity(10.0) pyramiding = -1 ) data chart = subscribe(data.ohlcv) on chart.close { if chart.close < ta.sma(chart.close, 50) { strategy.entry("Add", direction.long) } } ``` ## Fixed example ```flowscope script "Scale in" strategy ( defaultQty = qty.percentOfEquity(10.0) pyramiding = 3 ) data chart = subscribe(data.ohlcv) on chart.close { if chart.close < ta.sma(chart.close, 50) { strategy.entry("Add", direction.long) } } ``` ## Related - [B007](/docs/scripting/reference/diagnostics/b007) - [B004](/docs/scripting/reference/diagnostics/b004) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b006 --- # B007 > quantity is not positive **Severity** Error A quantity built with `qty.contracts`, `qty.cash`, `qty.percentOfEquity` or `qty.risk` has a constant value of zero or less. Direction is given by `direction.long` / `direction.short`, never by the sign of the quantity. Use a positive amount, and choose the direction with the `dir` argument. ## Failing example ```flowscope script "Short the pop" strategy initialCapital = 10_000.0 data chart = subscribe(data.ohlcv) on chart.close { if ta.rsi(chart.close, 14) > 80.0 { strategy.entry("Short", direction.long, qty: qty.contracts(-0.5)) } } ``` ## Fixed example ```flowscope script "Short the pop" strategy initialCapital = 10_000.0 data chart = subscribe(data.ohlcv) on chart.close { if ta.rsi(chart.close, 14) > 80.0 { strategy.entry("Short", direction.short, qty: qty.contracts(0.5)) } } ``` ## Related - [B006](/docs/scripting/reference/diagnostics/b006) - [B014](/docs/scripting/reference/diagnostics/b014) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b007 --- # B008 > commission or slippage is negative **Severity** Error `commission` and `slippage` model trading costs and must not be negative; a negative value would turn costs into income and make the backtest look better than any real account. Maker rebates are not modelled through negative commission. Use zero or a positive value. Slippage is given in ticks. ## Failing example ```flowscope script "Cost model" strategy ( commission = commission.percent(-0.01) slippage = 1 ) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.ema(chart.close, 50)) { strategy.entry("Long", direction.long) } if ta.crossunder(chart.close, ta.ema(chart.close, 50)) { strategy.close("Long") } } ``` ## Fixed example ```flowscope script "Cost model" strategy ( commission = commission.percent(0.02) slippage = 1 ) data chart = subscribe(data.ohlcv) on chart.close { let ema = ta.ema(chart.close, 50) if ta.crossover(chart.close, ema) { strategy.entry("Long", direction.long) } if ta.crossunder(chart.close, ema) { strategy.close("Long") } } ``` ## Related - [B014](/docs/scripting/reference/diagnostics/b014) - [B011](/docs/scripting/reference/diagnostics/b011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b008 --- # B009 > exit target given in ticks and as a price **Severity** Error `strategy.exit` accepts the take-profit either as a distance in ticks (`profit:`) or as an absolute price (`limit:`), and the stop either as `loss:` (ticks) or `stop:` (price). Passing both forms for the same leg leaves it unclear which one applies. Keep one form per leg. ## Failing example ```flowscope script "Fixed bracket" strategy defaultQty = qty.percentOfEquity(25.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.ema(chart.close, 20)) { strategy.entry("Long", direction.long) } let target = strategy.position.avgPrice * 1.02 strategy.exit("TP/SL", from: "Long", profit: 300.0, limit: target, loss: 150.0) } ``` ## Fixed example ```flowscope script "Fixed bracket" strategy defaultQty = qty.percentOfEquity(25.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.ema(chart.close, 20)) { strategy.entry("Long", direction.long) } let target = strategy.position.avgPrice * 1.02 strategy.exit("TP/SL", from: "Long", limit: target, loss: 150.0) } ``` ## Related - [B010](/docs/scripting/reference/diagnostics/b010) - [B005](/docs/scripting/reference/diagnostics/b005) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b009 --- # B010 > trailOffset without trailPoints **Severity** Error A trailing stop is activated once price has moved `trailPoints` ticks in favour of the position, and then follows price at a distance of `trailOffset` ticks. An offset without an activation distance never activates. Add `trailPoints:`, or remove `trailOffset:`. ## Failing example ```flowscope script "Trailing long" strategy defaultQty = qty.percentOfEquity(20.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.sma(chart.close, 30)) { strategy.entry("Long", direction.long) } strategy.exit("Trail", from: "Long", trailOffset: 50.0, loss: 200.0) } ``` ## Fixed example ```flowscope script "Trailing long" strategy defaultQty = qty.percentOfEquity(20.0) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.sma(chart.close, 30)) { strategy.entry("Long", direction.long) } strategy.exit("Trail", from: "Long", trailPoints: 100.0, trailOffset: 50.0, loss: 200.0) } ``` ## Related - [B009](/docs/scripting/reference/diagnostics/b009) - [B005](/docs/scripting/reference/diagnostics/b005) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b010 --- # B011 > leverage out of range **Severity** Error `leverage` must lie between 1 and 125. Values below 1 are not leverage, and values above 125 exceed what any supported venue offers. Leverage determines the margin used per position and, with `marginCall = true`, where the emulator liquidates. Choose a value from 1.0 to 125.0. ## Failing example ```flowscope script "Degen mode" strategy ( initialCapital = 1_000.0 leverage = 200.0 ) data chart = subscribe(data.ohlcv) on chart.close { if chart.close > chart.open { strategy.entry("Long", direction.long) } } ``` ## Fixed example ```flowscope script "Degen mode" strategy ( initialCapital = 1_000.0 leverage = 20.0 ) data chart = subscribe(data.ohlcv) on chart.close { if chart.close > chart.open { strategy.entry("Long", direction.long) } } ``` ## Related - [B014](/docs/scripting/reference/diagnostics/b014) - [B008](/docs/scripting/reference/diagnostics/b008) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b011 --- # B012 > processOrdersOnClose combined with the bar magnifier **Severity** Warning With `processOrdersOnClose = true`, market orders fill at the close of the bar that triggered them, and the bar magnifier (`magnifier = true`, the default when minute data exists) walks the real one-minute path inside each bar for limit and stop orders. Together, the decision and the fill happen on the same closing price while intrabar stops are checked minute by minute; the results can look better than what a live account would achieve. Turn one of the two off. For realistic fills keep the magnifier and fill market orders at the next open (`processOrdersOnClose = false`). The script still runs; this is a warning. ## Failing example ```flowscope script "Close fills" strategy ( processOrdersOnClose = true magnifier = true ) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.ema(chart.close, 34)) { strategy.entry("Long", direction.long) } strategy.exit("Stop", from: "Long", loss: 120.0) } ``` ## Fixed example ```flowscope script "Close fills" strategy ( processOrdersOnClose = false magnifier = true ) data chart = subscribe(data.ohlcv) on chart.close { if ta.crossover(chart.close, ta.ema(chart.close, 34)) { strategy.entry("Long", direction.long) } strategy.exit("Stop", from: "Long", loss: 120.0) } ``` ## Related - [B004](/docs/scripting/reference/diagnostics/b004) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b012 --- # B013 > strategy position read in setup **Severity** Error `strategy.position.*`, `strategy.equity`, `strategy.netProfit` and the other read-only strategy values change from bar to bar as the emulator fills orders. Setup values are computed once before the first bar, when there is no position yet. Read these values inside a handler. To remember a starting value, store it in `state` on the first bar. ## Failing example ```flowscope script "Equity guard" strategy initialCapital = 10_000.0 data chart = subscribe(data.ohlcv) setup startEquity = strategy.equity on chart.close { if strategy.equity < startEquity * 0.8 { strategy.closeAll(comment: "drawdown guard") } if strategy.position.size == 0.0 && chart.close > chart.open { strategy.entry("Long", direction.long) } } ``` ## Fixed example ```flowscope script "Equity guard" strategy initialCapital = 10_000.0 data chart = subscribe(data.ohlcv) setup startEquity = 10_000.0 on chart.close { if strategy.equity < startEquity * 0.8 { strategy.closeAll(comment: "drawdown guard") } if strategy.position.size == 0.0 && chart.close > chart.open { strategy.entry("Long", direction.long) } } ``` ## Related - [S001](/docs/scripting/reference/diagnostics/s001) - [B001](/docs/scripting/reference/diagnostics/b001) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b013 --- # B014 > initial capital is not positive **Severity** Error `initialCapital` is the account balance the backtest starts with, in the strategy `currency`. Percent-of-equity sizing, leverage, margin calls and every return figure in the Strategy Tester are computed from it, so it must be greater than zero. Use a positive amount. ## Failing example ```flowscope script "Empty account" strategy ( initialCapital = 0.0 defaultQty = qty.percentOfEquity(50.0) ) data chart = subscribe(data.ohlcv) on chart.close { if chart.close > ta.sma(chart.close, 100) { strategy.entry("Long", direction.long) } } ``` ## Fixed example ```flowscope script "Empty account" strategy ( initialCapital = 5_000.0 defaultQty = qty.percentOfEquity(50.0) ) data chart = subscribe(data.ohlcv) on chart.close { if chart.close > ta.sma(chart.close, 100) { strategy.entry("Long", direction.long) } } ``` ## Related - [B007](/docs/scripting/reference/diagnostics/b007) - [B008](/docs/scripting/reference/diagnostics/b008) - [B011](/docs/scripting/reference/diagnostics/b011) Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/b014 --- # I000 > internal compiler error **Severity** Error The compiler hit an unexpected condition while checking the script. This is a defect in Flowscope, not in your script. The message contains a short reference that identifies the failure. Try a small edit (or restart the script) to run the check again. If the error persists, report it with the reference and, if possible, the script, via the issue tracker. No particular script is known to produce this code; the examples below only show the shape of a report. ## Failing example ```flowscope script "Any script" // a script that, in some build, made the compiler fail internally data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { c.plot(chart.close) } ``` ## Fixed example ```flowscope script "Any script" // the same script checks fine after the defect is fixed data chart = subscribe(data.ohlcv) plot c = plot.line(title: "Close") on chart.close { c.plot(chart.close) } ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/scripting/reference/diagnostics/i000 --- # Data > One source for every market: Capture archives every frame, Derive prepares history from it, the data hub serves both. Flowscope has one source of market data. Every chart, bar, footprint, heatmap and figure you see comes from it, live and history alike: ```text venues → Capture → raw archive → Derive → derived history └──────────┬───────────┘ data hub → website (/hub) → the terminal ``` | Piece | What it does | |---|---| | **Capture** | The only process that talks to the exchanges for market data. It files every websocket frame of every captured market, untouched, with the time it arrived, plus the REST answers it took, into the raw archive: one file per market and hour. | | **Raw archive** | The one source of truth. Sealed hours never change; every view is derived from them and can be derived again after a fix. | | **Derive** | Replays every sealed hour through the same engine a live chart runs: books, liquidity runs, minute footprints, context, modeled liquidation levels. It writes day segments, heatmap tiles at several resolutions and seconds bars. | | **Data hub** | Tails the archive for live streams and serves the derived history. It never asks an exchange for anything. | ## What this means for a chart - **History begins where the archive does.** No venue publishes historical order books, and Flowscope builds bars and footprints from the trades it recorded, not from a venue's candles. Before a market was captured, its chart has no bars. - **Nothing is filled from elsewhere.** A minute Capture did not cover stays **Unavailable**; a minute it covered only in part is **Partial**. - **Live and history agree.** Both come from the same archive through the same engine, so the heatmap, footprints and bars do not jump where history meets the live part. ## What is kept | Data | Kept as | Served at | |---|---|---| | Minute footprints with trade counts and size bands, minute context (open interest, funding, liquidations, depth) | One day segment per market and UTC day | [`/v1/segments/…`](/docs/api/segments) | | Order-book liquidity and modeled liquidation levels | Hourly heatmap tiles at full, 5 s, 1 min, 5 min and 1 h resolution | `/v1/heatmap/…`, `/v1/liqmap/…` | | Seconds bars (1–45 s) and prepared footprint views | Hourly and daily prepared files | `/v1/chart/…` | | Hyperliquid take-profit, stop and liquidation levels, positioning footprints | Level runs and footprint minutes per coin and day | [`/v1/hl/…`](/docs/api/hl-levels) | | BTC and ETH option chains on Deribit, once a minute | Snapshots per coin and day | [`/v1/options/…`](/docs/api/options) | ```cards [Hyperliquid](/docs/data/hyperliquid) Measured TP, SL and liquidation maps and positioning footprints. [Files](/docs/data/storage) What Derive writes and the formats the API serves. [API](/docs/api) The data hub's HTTP API. ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/data --- # Hyperliquid > Measured take-profit, stop and liquidation maps and positioning footprints for Hyperliquid coins. Hyperliquid is transparent. Every trade names its buyer and seller, and any account's positions and orders can be read. Capture uses that to measure where real positions keep their take-profits, stops and liquidation prices, and what each trade did to open interest. Clients draw these as the [Hyperliquid heatmaps and positioning footprints](/docs/app/liquidations#hyperliquid-maps). Capture collects them for its configured coins from the complete websocket `trades` stream, seeded with the leaderboard's accounts worth $50k or more and kept current by applying every trade between reads. The data hub serves what it collected; the terminal never asks Hyperliquid for positions itself. ## How it works ### Discovery Every trade names both accounts. Capture also seeds the leaderboard: about 47 000 accounts, of which those worth at least $50k (about 17 000) are seeded. Each account keeps a measure of its recent traded notional with a one-hour half-life. ### Reading The most overdue account is read first. Position reads repeat faster for larger exposure in the recorded coins and after the account traded: | Exposure | Position read about every | |---|---| | $100M | 20 seconds | | $1M | Minute | | $10k | 10 minutes | | Flat | Hour | Never-read accounts alternate with refreshes, largest first. Orders are read only for accounts holding a recorded coin, from every minute ($1M and up) to hourly, and less often after two empty reads. ### Keeping positions current With the complete stream, every trade after a read is applied to both accounts. Trades that arrive while a read is in flight are replayed onto its answer. If the websocket drops, every account is marked unsynced until it is read again, and the connection is retried with a back-off of up to 30 seconds. ### Levels Every 10 seconds each coin is sampled. Per price bucket (0.02 % of the price, rounded to a step that stays fixed per coin) the collector sums the USD notional of: | Kind | Meaning | |---|---| | Long TP | Take-profit of a long: a sell above the price | | Short TP | Take-profit of a short: a buy below the price | | Long SL | Stop of a long: a sell below the price | | Short SL | Stop of a short: a buy above the price | | Long liquidation | Liquidation price of a long, `\|size\| × liquidation price` | | Short liquidation | Liquidation price of a short | Position TP/SL orders count the whole position; sized ones count at most the position. Triggers that cannot close the position, and levels more than 4× away from the mark, are ignored. Samples become runs like the order-book heatmap. A run is cut when its bucket changes by more than 5 %, empties, reaches UTC midnight, or when sampling stopped for more than 5 minutes. ### Footprints Per minute and price row (0.005 % of the price), from the complete stream: | Figure | Meaning | Counted when | |---|---|---| | Δ OI | Buyer's plus seller's change of long size | Both accounts are synced, so exact where counted | | Δ net long | Longs opened minus closed by the aggressor | The aggressor is synced | | Δ net short | Shorts opened minus closed by the aggressor | The aggressor is synced | A flip counts as a close plus an open. Each minute also stores Hyperliquid's open interest, the mark price, the tracked accounts' long and short size, and the volume each figure covers. ## Budgets {#budgets} Hyperliquid documents 1 200 weight per minute per IP. Measured from a single machine: - 3 000 `clearinghouseState` reads in 72 seconds passed. - `frontendOpenOrders` passed at 2 per second for 30 seconds, and was refused (HTTP 429) after about 180 requests at 4 per second. A refusal lasted about 40 seconds. The collector stays within the documented limit: 400 weight a minute of position reads, 680 of order reads and 120 for marks and the rest. A 429 empties the affected budget and pauses it for 60 seconds. ## Coverage Coverage is the share of Hyperliquid's open interest held by tracked accounts: the larger of tracked long and tracked short size, divided by open interest. It tells you how complete the maps are. Measured over 15 minutes live with these budgets on BTC, ETH, SOL and HYPE: | Measure | Value | |---|---| | Leaderboard seed | 16 996 of 47 189 accounts | | Accounts read | 2 406 | | Holding positions | 760 (5 600+ positions over all coins) | | Order reads | 588 | | Accounts with TP/SL | 78, with 352 triggers | | Requests | 5 488, 0 refused | | Coverage after 1 minute | 20–28 % | | Coverage after 11 minutes | 31–40 % | | Coverage after 15 minutes | 32–41 % (BTC 32 %, ETH 41 %, SOL 39 %, HYPE 35 %) | | Footprint coverage per 5-minute bar | BTC 26–82 %, ETH 22–48 %, HYPE 12–55 %, SOL 12–13 % | | Δ OI coverage | 0–49 % | The largest accounts are read first, so coverage rises quickly and then the tail grows slowly. Check live coverage with [`GET /v1/hl/status`](/docs/api/hl-status). ## Storage and serving Each coin has its own directory in the archive's `auxiliary/hl//` with a fixed grid and day files of appended payloads. See [Files](/docs/data/storage#hyperliquid-files). | Endpoint | Answer | |---|---| | [`/v1/hl/coins`](/docs/api/hl-coins) | Coins collected | | [`/v1/hl/status`](/docs/api/hl-status) | Accounts, requests and coverage per coin | | [`/v1/hl/levels/`](/docs/api/hl-levels) | Level runs in a time range | | [`/v1/hl/footprint/`](/docs/api/hl-footprint) | Footprint minutes in a time range | Answers for one hour are about 12 kB of levels and 2–4 kB of footprints per coin. ## On the client The terminal loads the last 3 hours of a coin from the hub, refreshes the tail every 10 seconds and loads older history six hours per request as the chart scrolls back. A coin the hub does not serve (404) has no maps. Source: https://flowscope.91-99-118-129.sslip.io/docs/data/hyperliquid --- # Files > What Derive writes from the raw archive, what Capture keeps beside it, and the formats the API serves. The data hub serves two directories: Derive's output, prepared from the raw archive, and the archive's `auxiliary` directory, where Capture keeps Hyperliquid positions and option chains. Neither is served as a directory; the hub's [API](/docs/api) is the only way to them. ## Derived history ```text data/ ├── archive-source.json ├── BinanceUsdm_BTCUSDT/ │ ├── 2026-10-03.fsd │ ├── 2026-10-04.fsd │ ├── heat/2026-10-04/13.book.fht │ ├── heat/2026-10-04/13.book.5s.fht │ ├── heat/2026-10-04/13.liq.fht │ └── chart-hours/… └── Okx_BTC-USDT-SWAP/ └── … ``` | Path | Content | Served at | |---|---|---| | `/.fsd` | Day segment of one market and one UTC day | [`/v1/segments/`](/docs/api/segments) | | `/heat//..fht` and its `.5s`, `.60s`, `.300s`, `.3600s` versions | One hour of order-book liquidity (`book`) or modeled liquidation levels (`liq`) as runs | `/v1/heatmap/`, `/v1/liqmap/` | | `/chart-hours/…`, day and block views | Seconds bars and prepared footprint views | `/v1/chart/` | | `archive-source.json` | The raw archive this output was derived from | — | Everything here can be derived again from the archive, after a fix or a new format; nothing in it is the only copy of anything. ### Market keys A market's key is `_`, with any character in the symbol other than ASCII letters, digits and `-` replaced by `_`. Examples: `BinanceUsdm_BTCUSDT`, `Okx_BTC-USDT-SWAP`, `Hyperliquid_BTC`. ### Day segments A day segment (`.fsd`) holds one market's history for one UTC day: - **Minute footprints**: per minute, OHLC, buy and sell volume, print count, buy and sell trade counts, buy and sell volume in each of the 11 trade-size bands, and buy/sell volume per price row. - **Minute context**: per minute, the last open interest, the last funding rate, long and short liquidations, and book depth within ±0.1, 0.25, 0.5, 1, 2, 2.5, 5 and 10 % of the mid. It is a compact little-endian binary format with a versioned header, DEFLATE-compressed, with an FNV-1a checksum. Only minutes that hold prints are written. The candle section is written empty: bars are built from the minutes. Derive writes each segment to a temporary file, syncs it and renames it over the old one, so a reader never sees a half-written segment, and merges so the more complete minute wins. The byte layout is in [Segment format](/docs/api/segment-format). A busy BTC perpetual takes about 0.04 MB per day compressed. ### Heatmap tiles Each tile holds one market's runs of one kind for one hour: start, duration, price bucket, size and side, stored column by column, DEFLATE-compressed, with a checksum. The coarser versions snap runs to 5 s, 1 min, 5 min or 1 h and keep the largest size per bucket and slot, so a zoomed-out heatmap looks the same with far fewer runs. A busy BTC perpetual takes about 125 MB of tiles a day. ## Hyperliquid files {#hyperliquid-files} Under `auxiliary/hl//` in the raw archive. ### `grid` A text file with two numbers: the level bucket size and the footprint row size in price units, for example: ```text 20 5 ``` The grid is chosen from the price on the first sample and then stays fixed for the coin, so every day file and every answer share one grid. Payloads written with a different grid are skipped when answers are built. ### `.levels` and `.fp` day files Day files are [positions codec](/docs/api/positions-codec) payloads appended one after another: - `.levels`: level runs that ended that day, appended on every save. - `.fp`: footprint minutes of that day, appended once each minute has closed. Each payload is self-delimiting and carries its own checksum. A reader stops at the first damaged or truncated payload, so a crash in the middle of a write loses only the last append. Appends are synced to disk. ## Option chain files {#option-chain-files} Under `auxiliary/options//` in the raw archive. Once a minute Capture asks Deribit for the option trades since its last request and for every listed option of each configured coin, and appends the chain with each option's net taker flow, as one snapshot, to that coin's file for the UTC day. An `.opt` day file is records written one after another, oldest first: ```text u32 length | snapshot (length bytes) ``` The length is little-endian. Each snapshot is exactly what [`/v1/options/`](/docs/api/options#snapshot-format) serves: the time it was taken, the index price and every option's expiry, strike, call or put, open interest, mark volatility, underlying price and flow, compressed, with its own checksum. The flow and its trade cursor are checkpointed together before each snapshot is appended, so a restart goes on exactly where it stopped. A reader stops at the first record whose length runs past the end of the file, so a crash in the middle of an append loses only that record. A BTC snapshot of about 950 options is about 10 to 12 kB, so a day file is about 14 to 17 MB. The hub keeps the newest snapshot of each coin in memory and reads older ones from the day files when asked for. Source: https://flowscope.91-99-118-129.sslip.io/docs/data/storage --- # API > The data hub's HTTP API: base URL, keys, conventions, status codes and examples. The data hub serves every market and its recorded history over one API: the same paths the terminal reads. It serves only what Capture archived and Derive prepared ([Data](/docs/data)); nothing is fetched from an exchange on your behalf. ## Base URL and keys | From | Base URL | Authentication | |---|---|---| | A signed-in browser page of this website | `https:///hub` | Your session cookie | | A script or tool of yours | `https:///hub` | Your session token: `Authorization: Bearer ` | | A key issued for the hub | the hub's own address | `X-Api-Key: fsk_…` or `?key=fsk_…` | Every path below is relative to that base and starts with `/v1/`. A request without a valid key or session answers `401`. Limits (connections, subscriptions, request weight per minute, history reach) depend on the key's tier and count per account. ## Endpoints | Method | Path | Answer | Type | |---|---|---|---| | `GET` | [`/v1/health`](/docs/api/health) | `ok feeds N sessions M` | Text | | `GET` | [`/v1/markets`](/docs/api/markets) | Every market the hub serves | JSON | | `GET` | [`/v1/segments//`](/docs/api/segments#list-days) | Days held for a market, one per line | Text | | `GET` | [`/v1/segments//.fsd`](/docs/api/segments#get-a-day) | One day segment | Binary | | `GET` | [`/v1/hl/coins`](/docs/api/hl-coins) | Hyperliquid coins collected, one per line | Text | | `GET` | [`/v1/hl/status`](/docs/api/hl-status) | Accounts, requests and coverage per coin | Text | | `GET` | [`/v1/hl/levels/?from=&to=`](/docs/api/hl-levels) | TP, SL and liquidation runs | Binary | | `GET` | [`/v1/hl/footprint/?from=&to=`](/docs/api/hl-footprint) | OI, net-long and net-short footprint minutes | Binary | | `GET` | [`/v1/options/[?at=]`](/docs/api/options) | A coin's Deribit option chain, the newest or the newest at `at` | Binary | The hub also serves candles, the live websocket stream, heatmap tiles and seconds charts to the terminal; those are described for operators in the repository (`docs/HUB.md`). ## Conventions | Topic | Rule | |---|---| | Text answers | `text/plain; charset=utf-8`, one item per line, each line ending in `\n`. An empty list is an empty body. | | JSON answers | `application/json`. Errors are `{"error":{"code","message"}}`. | | Binary answers | `application/octet-stream`, little-endian. See [Segment format](/docs/api/segment-format), [Positions codec](/docs/api/positions-codec) and [option snapshots](/docs/api/options#snapshot-format). | | Market keys | `_`, for example `BinanceUsdm_BTCUSDT`. Only ASCII letters, digits, `_` and `-`, shorter than 128 characters. | | Days | `YYYY-MM-DD`, UTC. | | Times | Milliseconds since the Unix epoch, UTC, as integers. | | Paths | Matched literally. Percent-encoding is not decoded; `..`, `%`, `\` and control characters answer `400`. | ## Status codes | Status | When | |---|---| | `200` | Success | | `304` | `If-None-Match` named the current `ETag` | | `400` | Invalid market key, day or range | | `401` | No valid key or session | | `403` | The account has no market data, or the range is beyond the tier's history | | `404` | Unknown path, or a day, coin or snapshot that is not there | | `429` | Over the tier's request weight; `Retry-After` says when to come back | | `503` | History reads are at capacity, or a collected coin has no sample yet; `Retry-After` when it is a matter of seconds | Use the status code in programs; bodies are meant for people. ## Quick tour ```bash BASE=https://flowscope.example/hub AUTH="Authorization: Bearer $FLOWSCOPE_TOKEN" curl $BASE/v1/health curl -H "$AUTH" $BASE/v1/markets curl -H "$AUTH" $BASE/v1/segments/BinanceUsdm_BTCUSDT/ curl -H "$AUTH" -o 2026-10-03.fsd $BASE/v1/segments/BinanceUsdm_BTCUSDT/2026-10-03.fsd curl -H "$AUTH" $BASE/v1/hl/coins FROM=$(( ($(date +%s) - 3600) * 1000 )); TO=$(( $(date +%s) * 1000 )) curl -H "$AUTH" -o btc.levels "$BASE/v1/hl/levels/BTC?from=$FROM&to=$TO" curl -H "$AUTH" -o btc.opt $BASE/v1/options/BTC ``` ## Versioning Paths carry the API version (`/v1/`). Binary payloads carry their own format version in their header: day segments are at version 3, Hyperliquid payloads at version 1, option snapshots at `FSO2` (the version is in the magic). Source: https://flowscope.91-99-118-129.sslip.io/docs/api --- # GET /v1/health > A liveness check that answers while the data hub accepts requests. Answers while the data hub accepts requests. It needs no key: use it for load balancers and uptime monitors. ## Request ```text GET /v1/health ``` ## Response `200`, `text/plain; charset=utf-8`: ```text ok feeds 12 sessions 340 ``` `feeds` is the number of markets the hub follows right now, `sessions` the open websocket sessions. While the hub shuts down it answers `503` with `gateway draining`: a balancer then sends new clients to another hub, and connected clients resume there. ## Example ```bash curl -fsS https://flowscope.example/hub/v1/health && echo up ``` ## What it does not tell you `/v1/health` says the hub is up, not that a market is current. To see that a market's history grows, list its days with [`/v1/segments//`](/docs/api/segments#list-days) and fetch today's segment; for Hyperliquid read [`/v1/hl/status`](/docs/api/hl-status). Source: https://flowscope.91-99-118-129.sslip.io/docs/api/health --- # GET /v1/markets > Every market the data hub serves, with its base, quote, kind and 24-hour volume. Lists every market the hub serves: the markets Capture archives. A market that is not here has no stream and no history on this hub. ## Request ```text GET /v1/markets ``` ## Response `200`, `application/json`, `Cache-Control: private, max-age=10`: ```json { "markets": [ {"market": "binancef:BTCUSDT", "base": "BTC", "quote": "USDT", "perpetual": true, "volume_24h": 1.2e10}, {"market": "okxf:BTC-USDT-SWAP", "base": "BTC", "quote": "USDT", "perpetual": true, "volume_24h": 4.1e9} ] } ``` | Field | Meaning | |---|---| | `market` | The hub's market name, `:` | | `base`, `quote` | The traded pair | | `perpetual` | A perpetual future rather than spot | | `volume_24h` | The venue's 24-hour volume in the quote currency, refreshed each minute | History paths name a market by its key, `_` (for example `BinanceUsdm_BTCUSDT`), with any character in the symbol other than ASCII letters, digits and `-` replaced by `_`. ## Example ```bash curl -fsS -H "Authorization: Bearer $FLOWSCOPE_TOKEN" https://flowscope.example/hub/v1/markets ``` Source: https://flowscope.91-99-118-129.sslip.io/docs/api/markets --- # GET /v1/segments > List the days held for a market, and download one day segment. Day segments hold one market's minute footprints and minute context for one UTC day. This endpoint lists the days a market has and serves each day as a binary file. ## List days {#list-days} ```text GET /v1/segments// HEAD /v1/segments// ``` | Parameter | In | Description | |---|---|---| | `key` | Path | Market key, for example `BinanceUsdm_BTCUSDT`. See [`/v1/markets`](/docs/api/markets). | The trailing slash is optional: `/v1/segments/` lists the days too. ### Response | Header | Value | |---|---| | `Content-Type` | `text/plain; charset=utf-8` | | `Cache-Control` | `no-cache` | One day per line as `YYYY-MM-DD`, sorted oldest first. A key with no directory gives `200` with an empty body, not `404`. ```bash curl -H "$AUTH" $BASE/v1/segments/BinanceUsdm_BTCUSDT/ ``` ```text 2026-10-01 2026-10-02 2026-10-03 2026-10-04 ``` ### Status codes | Status | Body | When | |---|---|---| | `200` | The list | Valid key | | `400` | `bad market key` | The key is empty, 128 characters or longer, or has characters other than ASCII letters, digits, `_` and `-` | ## Get a day {#get-a-day} ```text GET /v1/segments//.fsd HEAD /v1/segments//.fsd ``` | Parameter | In | Description | |---|---|---| | `key` | Path | Market key | | `YYYY-MM-DD` | Path | The UTC day, followed by `.fsd` | ### Response | Header | Value | |---|---| | `Content-Type` | `application/octet-stream` | | `Cache-Control` | Days before yesterday: `public, max-age=86400`. Today and yesterday (UTC): `no-cache`. | | `ETag` | The segment's checksum and length; `If-None-Match` with it answers `304` | The body is the day segment as Derive wrote it. Decode it as described in [Segment format](/docs/api/segment-format). It ends in a checksum, so you can verify a download completely. Today's segment grows: the hub merges what Derive wrote with the minutes the market holds now, so the answer includes the current hour. Flowscope clients ask for it again every two minutes. Past days are final but not immutable: a rebuild after a fix may change them, which the `ETag` shows. ### Status codes | Status | Body | When | |---|---|---| | `200` | The segment | The day is recorded | | `400` | `bad market key` | Invalid key | | `400` | `bad day` | The file name is not `YYYY-MM-DD.fsd` with a valid month (1–12) and day (1–31) | | `404` | `not recorded` | Valid key and day, but nothing archived that day. Sent with `Cache-Control: no-cache`, so a cache asks again later. | ### Example ```bash curl -fsS -H "$AUTH" -o BinanceUsdm_BTCUSDT-2026-10-03.fsd \ $BASE/v1/segments/BinanceUsdm_BTCUSDT/2026-10-03.fsd xxd BinanceUsdm_BTCUSDT-2026-10-03.fsd | head -4 ``` ```text 00000000: 4653 4431 0300 0100 .... FSD1........ ``` The file starts with the magic `FSD1`, version `3` and codec `1`: the rest is compressed. See [Segment format](/docs/api/segment-format#compressed) for how to inflate and read it. ## Building URLs Clients build a segment URL from the base URL, the market key and the day: ```text /v1/segments/_/.fsd ``` The day of a timestamp is `floor(ms / 86 400 000)` days after 1970-01-01, in UTC. Any trailing `/` on the base is dropped first. ## How Flowscope clients use it For each day a chart wants, a client: 1. asks the data hub, up to four days in flight per market; 2. merges the segment into its minute store, where the more complete minute wins; 3. asks again for today after two minutes, and for a missing day after ten minutes; 4. shows minutes the archive does not cover as `Unavailable`; nothing is fetched from a venue instead. Clients keep segments in memory for the session only; they are not stored on the machine. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/segments --- # GET /v1/hl/coins > The Hyperliquid coins whose positions Capture collects, one per line. Lists the Hyperliquid coins whose positions Capture collects and the hub serves. Each one can be used with [`/v1/hl/levels`](/docs/api/hl-levels) and [`/v1/hl/footprint`](/docs/api/hl-footprint). ## Request ```text GET /v1/hl/coins HEAD /v1/hl/coins ``` No parameters. ## Response | Header | Value | |---|---| | `Content-Type` | `text/plain; charset=utf-8` | | `Cache-Control` | `no-cache` | One coin per line, sorted, each line ending in a newline. Coins are Hyperliquid's own names, for example `BTC`, `ETH`, `HYPE` or `kPEPE`. ## Status codes | Status | Body | When | |---|---|---| | `200` | The list | Positions are collected | | `404` | `hyperliquid positions not recorded` | No coin is collected | ## Example ```bash curl -H "$AUTH" $BASE/v1/hl/coins ``` ```text BTC ETH HYPE SOL ``` Fetch the last hour of levels for every coin: ```bash BASE=https://flowscope.example/hub TO=$(( $(date +%s) * 1000 )) FROM=$(( TO - 3600000 )) for coin in $(curl -fsS -H "$AUTH" $BASE/v1/hl/coins); do curl -fsS -H "$AUTH" -o "$coin.levels" "$BASE/v1/hl/levels/$coin?from=$FROM&to=$TO" done ``` ## Notes - A coin is listed as soon as the hub starts, before its first sample. Until then, [`/v1/hl/levels`](/docs/api/hl-levels) and [`/v1/hl/footprint`](/docs/api/hl-footprint) answer `503 no sample yet` for it. - This list is about positions. It is separate from the `Hyperliquid_` markets in [`/v1/markets`](/docs/api/markets), which hold footprints and context. - Flowscope clients map any market to its Hyperliquid coin: `BTCUSDT`, `BTC-USDT-SWAP` and the aggregate `BTC` all map to `BTC`; `1000PEPE` maps to `kPEPE`. A client asks the data hub for a coin; on `404` it has no maps for it. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/hl-coins --- # GET /v1/hl/status > Plain-text status of Hyperliquid position recording: accounts, requests and coverage per coin. Reports how far Capture's Hyperliquid collector has got: how many accounts it knows and has read, how many requests it made and how many were refused, and per coin how much of the venue's open interest its maps cover. ## Request ```text GET /v1/hl/status HEAD /v1/hl/status ``` No parameters. ## Response | Header | Value | |---|---| | `Content-Type` | `text/plain; charset=utf-8` | | `Cache-Control` | `no-cache` | The body is text, one `name value` pair per line, followed by one line per coin. ### Collector lines | Name | Meaning | |---|---| | `accounts` | Accounts known to the collector, from trades and the leaderboard seed | | `accounts_read` | Accounts whose positions were read at least once | | `synced` | Accounts whose positions are current: read, and kept up to date from the trade stream since | | `with_positions` | Accounts holding at least one position | | `positions` | Positions held by those accounts, all coins | | `orders_read` | Accounts whose orders were read at least once | | `with_triggers` | Accounts with at least one TP or SL order | | `triggers` | TP and SL orders counted | | `requests` | Requests sent to Hyperliquid since start | | `failures` | Requests that failed | | `refused` | Requests refused by Hyperliquid's rate limit (HTTP 429) | | `trades` | Trades received from the websocket stream | ### Coin lines Each coin line starts with `coin ` and continues with `name value` pairs: | Name | Meaning | |---|---| | `holders` | Tracked accounts holding the coin | | `triggers` | Their TP and SL orders counted | | `tracked_long`, `tracked_short` | Long and short size of the tracked accounts, in coins, 4 decimals | | `oi` | Hyperliquid's open interest, in coins (one side), 4 decimals | | `coverage` | `max(tracked_long, tracked_short) / oi`, from 0 to 1, 3 decimals | | `runs` | Level runs held in memory | | `minutes` | Footprint minutes held in memory | | `latest` | Time of the newest sample, ms since the epoch (`0` before the first) | ## Status codes | Status | Body | When | |---|---|---| | `200` | The status | Hyperliquid recording is on | | `404` | `hyperliquid positions not recorded` | No coin is collected | ## Example ```bash curl -H "$AUTH" $BASE/v1/hl/status ``` ```text accounts 18231 accounts_read 2406 synced 2391 with_positions 760 positions 5614 orders_read 588 with_triggers 78 triggers 352 requests 5488 failures 2 refused 0 trades 214305 coin BTC holders 341 triggers 121 tracked_long 3412.5521 tracked_short 3015.0874 oi 10664.1203 coverage 0.320 runs 18210 minutes 15 latest 1759580110000 coin ETH holders 298 triggers 96 tracked_long 61120.3310 tracked_short 58301.9002 oi 149074.0002 coverage 0.410 runs 14822 minutes 15 latest 1759580110000 coin HYPE holders 187 triggers 74 tracked_long 2210334.1000 tracked_short 1980112.5000 oi 6315240.0000 coverage 0.350 runs 9310 minutes 15 latest 1759580110000 coin SOL holders 203 triggers 61 tracked_long 512330.2200 tracked_short 488010.9100 oi 1313667.0000 coverage 0.390 runs 10544 minutes 15 latest 1759580110000 ``` The numbers above are illustrative. The coverage figures match what was measured after 15 minutes of recording. ## Reading it - **Coverage** says how complete the maps are. It rises fast in the first minutes, because the largest accounts are read first, and then grows slowly as smaller accounts are found. - **`refused` above 0** means the budgets are too high for the IP. Each refusal pauses the affected budget for 60 seconds. Lower `--hl-state-per-min` or `--hl-orders-per-min`. - **`synced` well below `accounts_read`** after a while means the trade stream dropped. A gap marks every account unsynced until it is read again. - **`latest` not moving** means sampling stopped. Parse the collector lines as `name value`; split coin lines on spaces and read the pairs after `coin `. ## Related - [Data: Hyperliquid](/docs/data/hyperliquid#coverage) explains coverage and the measured numbers. - [`/v1/hl/coins`](/docs/api/hl-coins) lists the coins without the statistics. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/hl-status --- # GET /v1/hl/levels > Hyperliquid take-profit, stop-loss and liquidation level runs of one coin over a time range. Returns the measured take-profit, stop-loss and liquidation levels of one Hyperliquid coin as runs: how much USD notional sat in each price bucket, from when to when. Flowscope draws these as the [Hyperliquid heatmaps](/docs/app/liquidations#hyperliquid-maps). ## Request ```text GET /v1/hl/levels/?from=&to= HEAD /v1/hl/levels/?from=&to= ``` | Parameter | In | Type | Description | |---|---|---|---| | `COIN` | Path | string | A coin from [`/v1/hl/coins`](/docs/api/hl-coins), case-sensitive (`BTC`, `kPEPE`) | | `from` | Query | integer, ms | Start of the range, inclusive | | `to` | Query | integer, ms | End of the range, exclusive | Rules for the range: - `to` must be greater than `from`. - The range may span at most 60 days. - A missing or unparsable `to` defaults to a far-future sentinel, and a missing `from` to one day before `to`. Always pass both: `from` alone makes the range longer than 60 days and is rejected. ## Response | Header | Value | |---|---| | `Content-Type` | `application/octet-stream` | | `Cache-Control` | `no-cache` | The body is one **levels payload** in the [positions codec](/docs/api/positions-codec#levels-payload): a header with the coin, the bucket size, the newest sample time and the range answered, then the runs, then a checksum. What the payload contains: - Every run that **overlaps** the range: `start < to` and `start + duration > from`. - Runs sorted by their end time. - Runs still open are included, ending at the newest sample (`latest`). - The payload's `from` is the requested `from`, or later when nothing is recorded before that point. Use it to tell "no levels" from "not recorded yet". Each run has a kind: | Kind | Value | Meaning | |---|---|---| | Long TP | 0 | Take-profit of a long: a sell above the price | | Short TP | 1 | Take-profit of a short: a buy below the price | | Long SL | 2 | Stop of a long: a sell below the price | | Short SL | 3 | Stop of a short: a buy above the price | | Long liquidation | 4 | Liquidation of a long, below the price | | Short liquidation | 5 | Liquidation of a short, above the price | A run's bucket covers prices `[bucket × bucket_px, (bucket + 1) × bucket_px)`, and its `notional` is the total USD of the orders or positions in it. ## Status codes | Status | Body | When | |---|---|---| | `200` | Levels payload | The coin has samples | | `400` | `bad range` | `to <= from`, or the range is longer than 60 days | | `404` | `coin not recorded` | The coin is not collected | | `404` | `hyperliquid positions not recorded` | Hyperliquid recording is off | | `503` | `no sample yet` | The coin is recorded but has not been sampled yet | ## Example The last hour of BTC levels: ```bash TO=$(( $(date +%s) * 1000 )) FROM=$(( TO - 3600000 )) curl -fsS -H "$AUTH" -o btc.levels "$BASE/v1/hl/levels/BTC?from=$FROM&to=$TO" ls -l btc.levels ``` An hour is typically about 12 kB. The start of the file, annotated: ```text 46 53 50 4c "FSPL" 01 00 version 1 00 00 reserved 00 00 00 00 00 00 34 40 bucket_px = 20.0 ... latest, from, to (i64 each) 03 42 54 43 coin: length 3, "BTC" ... run count (u32), runs (21 bytes each), checksum ``` ### Decoding in Rust The `flowscope-engine` crate has the decoder: ```rust use flowscope_engine::positions::codec::decode_levels; let bytes = std::fs::read("btc.levels")?; let p = decode_levels(&bytes)?; println!("{} bucket {} from {} to {} latest {}", p.coin, p.bucket_px, p.from, p.to, p.latest); for r in &p.runs { let lo = f64::from(r.bucket) * p.bucket_px; println!("{:?} {lo}..{} ${:.0} from {} for {} ms", r.kind, lo + p.bucket_px, r.notional, r.start, r.dur); } ``` In any other language, follow the byte layout in [Positions codec](/docs/api/positions-codec#levels-payload). ## How answers are built Runs that ended before what memory holds completely come from the coin's `.levels` day files; the rest come from memory. Payloads in the day files with a different bucket size are skipped, so every answer uses one grid. Levels are sampled every 10 seconds. A Flowscope client loads the last 3 hours, then asks for the tail every 10 seconds and for older history six hours at a time. ## Related - [`/v1/hl/footprint`](/docs/api/hl-footprint) returns the positioning footprints of the same coin. - [Data: Hyperliquid](/docs/data/hyperliquid#levels) describes how levels are sampled. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/hl-levels --- # GET /v1/hl/footprint > Hyperliquid open-interest, net-long and net-short footprint minutes of one coin over a time range. Returns per minute and price row what Hyperliquid trades did to positions: the change in open interest, longs opened minus closed, and shorts opened minus closed. Flowscope draws these as the [Hyperliquid positioning footprints](/docs/app/liquidations#hyperliquid-positioning-footprints). ## Request ```text GET /v1/hl/footprint/?from=&to= HEAD /v1/hl/footprint/?from=&to= ``` | Parameter | In | Type | Description | |---|---|---|---| | `COIN` | Path | string | A coin from [`/v1/hl/coins`](/docs/api/hl-coins), case-sensitive | | `from` | Query | integer, ms | Start of the range; rounded down to the minute | | `to` | Query | integer, ms | End of the range; rounded up to the minute, exclusive | The range rules are the same as for [`/v1/hl/levels`](/docs/api/hl-levels#request): `to` greater than `from`, at most 60 days, and always pass both. ## Response | Header | Value | |---|---| | `Content-Type` | `application/octet-stream` | | `Cache-Control` | `no-cache` | The body is one **footprint payload** in the [positions codec](/docs/api/positions-codec#footprint-payload): the coin, the row size, the newest sample time and the start of the range answered, then the minutes, then a checksum. The payload holds every recorded minute that **starts** in the range, in time order. Each minute has: | Field | Unit | Meaning | |---|---|---| | `minute` | minutes since the epoch | `ms / 60 000` | | `oi` | coins | Hyperliquid's open interest at the end of the minute | | `mark` | price | Mark price at the end of the minute | | `tracked_long`, `tracked_short` | coins | Long and short size of the tracked accounts at the end of the minute | | `volume` | coins | Traded volume in the minute | | `taker_known` | coins | Volume whose aggressor was tracked: covered by net long and net short | | `both_known` | coins | Volume with both sides tracked: covered by Δ OI | and a list of price cells: | Field | Unit | Meaning | |---|---|---| | `row` | — | Prices `[row × row_px, (row + 1) × row_px)` | | `d_oi` | coins | Δ open interest: buyer's plus seller's change of long size | | `nl` | coins | Δ net long: longs opened minus closed by the aggressor | | `ns` | coins | Δ net short: shorts opened minus closed by the aggressor | | `volume` | coins | Traded volume in the row, all trades | `NaN` means unknown: `oi`, `mark`, `tracked_long` and `tracked_short` are `NaN` in a minute where they were not seen. Coverage per minute is `taker_known / volume` for net long and net short, and `both_known / volume` for Δ OI. Show it next to the figures; a chart without coverage overstates what it knows. The payload's `from` is the start of the requested range, or later when nothing is recorded before that point. ## Status codes | Status | Body | When | |---|---|---| | `200` | Footprint payload | The coin has samples | | `400` | `bad range` | `to <= from`, or the range is longer than 60 days | | `404` | `coin not recorded` | The coin is not collected | | `404` | `hyperliquid positions not recorded` | Hyperliquid recording is off | | `404` | `not found` | Anything other than `levels` or `footprint` after `/v1/hl/` | | `503` | `no sample yet` | The coin is recorded but has not been sampled yet | ## Example The last hour of ETH footprints: ```bash TO=$(( $(date +%s) * 1000 )) FROM=$(( TO - 3600000 )) curl -fsS -H "$AUTH" -o eth.fp "$BASE/v1/hl/footprint/ETH?from=$FROM&to=$TO" ``` An hour is typically 2–4 kB per coin. ### Decoding in Rust ```rust use flowscope_engine::positions::codec::decode_footprint; let bytes = std::fs::read("eth.fp")?; let p = decode_footprint(&bytes)?; for m in &p.minutes { let taker_cov = if m.volume > 0.0 { m.taker_known / m.volume } else { 0.0 }; let nl: f32 = m.cells.iter().map(|c| c.nl).sum(); let ns: f32 = m.cells.iter().map(|c| c.ns).sum(); println!("{} oi {} net long {nl:+.2} net short {ns:+.2} coverage {:.0} %", m.minute * 60_000, m.oi, taker_cov * 100.0); } ``` ## How answers are built Minutes that memory no longer holds completely come from the coin's `.fp` day files; the rest come from memory. A minute is appended to its day file once it has closed. Payloads written with a different row size are skipped. Footprints need the complete trade stream, which Capture follows; Flowscope clients get them from the data hub. ## Related - [`/v1/hl/levels`](/docs/api/hl-levels) returns the TP, SL and liquidation levels of the same coin. - [Data: Hyperliquid](/docs/data/hyperliquid#footprints) describes how footprints are computed. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/hl-footprint --- # GET /v1/options > A coin's whole Deribit option chain as recorded once a minute, with each option's recorded taker flow, the newest or the newest at a given time, as one binary snapshot. Returns one snapshot of a coin's option chain: every listed option with its expiry, strike, open interest, mark volatility, underlying price and recorded flow (net taker buying), as Capture took it from Deribit. Flowscope computes [gamma exposure](/docs/app/gamma-exposure) from it. Capture takes a snapshot of each configured coin (BTC and ETH by default) once a minute; see [Files](/docs/data/storage#option-chain-files). ## Request ```text GET /v1/options/[?at=] GET /v1/options/?from=&to=&step= HEAD /v1/options/[?at=] ``` | Parameter | In | Type | Description | |---|---|---|---| | `COIN` | Path | string | `BTC` or `ETH`, as collected; case-sensitive | | `at` | Query | integer, ms | Optional. The newest snapshot taken at or before this time. Without it, or if it does not parse, the newest snapshot. | With `at`, the snapshot is looked for in the UTC day of `at` and the day before. An `at` at or after the newest snapshot answers the newest. ### Over time {#over-time} With `from`, `to` and `step` (all milliseconds) the answer is the chains over `[from, to)`: the first snapshot of every `step` on the UTC grid, oldest first, at most 300 of them. `to - from` may be a day at most and `step` a minute or more; otherwise `400`. The body is a series of records, each `length u32` (little-endian) followed by that many bytes of one snapshot in the format below. A range with nothing recorded answers `200` with an empty body. Flowscope's Gamma Exposure study draws its exposure over time from these. ## Response | Header | Value | |---|---| | `Content-Type` | `application/octet-stream` | | `Cache-Control` | `no-cache` | The body is one snapshot in the format below. Its time (`ts`) says when it was taken; the hub keeps answering the last one there is if Deribit did not answer, so check `ts` if you need a fresh chain. A BTC snapshot of about 950 options is about 10 to 12 kB. ## Snapshot format {#snapshot-format} Little-endian, no padding. A 16-byte header, a raw DEFLATE stream holding the chain, and a checksum: ```text "FSO2" | ts i64 | raw length u32 | deflate(index f64 | n u32 | n × (expiry i64, strike f64, put u8, open interest f32, mark iv f32, underlying f64, flow f32)) | fnv1a-32 u32 ``` The reference implementation is `flowscope_engine::options` (`encode`, `decode`). ### Header | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 4 | bytes | magic | ASCII `FSO2`; the format's version is part of it | | 4 | 8 | i64 | ts | When the snapshot was taken, ms since the epoch | | 12 | 4 | u32 | raw length | Length of the chain after decompression: `12 + 37 × n` | | 16 | total − 20 | bytes | chain | The chain, raw DEFLATE (RFC 1951, no zlib or gzip wrapper) | ### Chain What the DEFLATE stream decompresses to: | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 8 | f64 | index | The coin's index price when the snapshot was taken (Deribit's estimated delivery price); `0` if Deribit gave none | | 8 | 4 | u32 | n | Number of options | | 12 | 37 × n | | options | One record per option, below | ### Option record 37 bytes each, in the order Deribit listed them. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 8 | i64 | expiry | Expiry, ms since the epoch: 08:00 UTC of the day in the option's name | | 8 | 8 | f64 | strike | Strike price, USD | | 16 | 1 | u8 | put | `0` for a call, `1` for a put; readers treat any non-zero value as a put | | 17 | 4 | f32 | open interest | Open interest in the coin (one Deribit BTC or ETH option is one coin) | | 21 | 4 | f32 | mark iv | Mark implied volatility, percent a year (`41.25` is 41.25 %) | | 25 | 8 | f64 | underlying | Price of what the option settles on: the future of its expiry | | 33 | 4 | f32 | flow | Net taker buying recorded, in the coin: the amount of every trade of this option Capture saw where the taker bought, minus every one where the taker sold, from when recording began. `0` where no trade was seen | Options without a mark volatility or an underlying price in Deribit's answer are left out. Options with no open interest are kept, with `0`. Capture follows Deribit's option trades (`get_last_trades_by_currency`, `kind=option`) once a minute, just before it takes the chain, and adds each trade's amount to its option's flow with the sign of the taker's `direction`. Block trades count too. The flow carries on across snapshots and is dropped when the option expires. The flow and its trade cursor are checkpointed together before each snapshot; after a restart Capture goes on from the checkpoint and asks for the trades since then. ### Trailer | Size | Type | Field | |---|---|---| | 4 | u32 | FNV-1a 32 of every byte of the snapshot before the trailer, from the magic on (the compressed chain, not the decompressed one) | ### Reading rules A reader rejects a snapshot that: - is shorter than 20 bytes or does not start with `FSO2`; - fails the checksum; - has a raw length below 12 or above 16 MiB; - does not decompress, or decompresses to anything but `12 + 37 × n` bytes. The name of each option on Deribit is `---`, for example `BTC-27NOV26-73000-P`: a put at 73 000 expiring on 27 November 2026 at 08:00 UTC. The record carries the parts, not the name. ## Status codes | Status | Body | When | |---|---|---| | `200` | The snapshot | A snapshot was found | | `404` | `options of are not recorded` | The coin is not collected, or has no options (only `BTC` and `ETH` do) | | `404` | `no snapshot yet` | The coin is recorded but has no snapshot: none taken yet, or none at or before `at` | | `404` | `option chains not recorded` | No coin's options are collected | | `404` | `not found` | The path names no coin (`/v1/options/`) or has more after it | | `400` | `from < to, a day at most, step a minute or more` | An [over time](#over-time) request outside those bounds | ## Example The newest BTC chain, and the one at a given time: ```bash curl -fsS -o btc.opt $BASE/v1/options/BTC curl -fsS -o btc-then.opt "$BASE/v1/options/BTC?at=1791000000000" xxd btc.opt | head -1 ``` ```text 00000000: 4653 4f32 0036 ebff a001 0000 e887 0000 FSO2.6.......... ``` The magic `FSO2`, the time (`0x01a0ffeb3600` = 1 791 000 000 000 ms), then the raw length (`0x87e8` = 34 792 bytes: 12 + 37 × 940, so 940 options). The values after the magic are illustrative; the compressed chain follows from offset 16. ### Decoding in Rust ```rust use flowscope_engine::options::{Expiries, Positioning, decode, gex}; let bytes = std::fs::read("btc.opt")?; let s = decode(&bytes)?; println!("{} options at {}, index {}", s.quotes.len(), s.ts, s.index); let g = gex(&s, s.ts, Expiries::All, Positioning::Flow); println!("call wall {:?} put wall {:?} flip {:?} net ${:.0} per 1 %", g.call_wall, g.put_wall, g.flip, g.total); ``` ### Decoding in Python ```python import struct, zlib b = open("btc.opt", "rb").read() assert b[:4] == b"FSO2" body, (fnv,) = b[:-4], struct.unpack("` once a minute while a chart shows the Gamma Exposure study, for the coin of the chart's market. After a failed request it waits a minute longer for each failure in a row, ten minutes at most. It does not use `at`. ## Related - [Gamma exposure](/docs/app/gamma-exposure) describes what the app computes from the snapshots. - [Data: files](/docs/data/storage#option-chain-files) describes the `.opt` day files the snapshots are kept in. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/options --- # Segment format > The byte layout of a day segment (.fsd): header, candle (empty), minute, cell and context records, and the checksum. A day segment holds one market's minute footprints and minute context for one UTC day, as one compact binary blob. Derive writes segments, the data hub serves them, and clients decode them on any platform. The reference implementation is `flowscope_engine::segment`. This page describes **version 3**. Files and the API carry it [compressed](#compressed); the layout below is what decompression gives. ## Overview ```text "FSD1" | version u16 | codec u16 (0 = raw) | day i64 | tick_size f64 | row_ticks i64 | base_tick i64 | candles u32 | minutes u32 | cells u32 | context u32 | candles × (minute u16, has_buy u8, 0 u8, open, high, low, close, volume, buy: f64) | minutes × (minute u16, state u8, 0 u8, prints u32, open, high, low, close: i32, buy f64, sell f64, cells u32, buy_trades u32, sell_trades u32, buy f32 × 11, sell f32 × 11) | cells × (row i32, buy f32, sell f32) | context × (minute u16, 0 u16, oi f64, funding f32, liq_long f32, liq_short f32, depth bid f32 × 8, depth ask f32 × 8) | fnv1a-32 of everything before, u32 ``` Everything is **little-endian**. There is no padding between records; the `0` fields are explicit reserved bytes. ## Header 56 bytes. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 4 | bytes | magic | ASCII `FSD1` | | 4 | 2 | u16 | version | `3` | | 6 | 2 | u16 | codec | `0` = raw (this layout). `1` = [compressed](#compressed). | | 8 | 8 | i64 | day | Days since 1970-01-01 (UTC) | | 16 | 8 | f64 | tick_size | The instrument's price tick | | 24 | 8 | i64 | row_ticks | Footprint row height in ticks, at least 1 | | 32 | 8 | i64 | base_tick | Reference tick for the day's prices | | 40 | 4 | u32 | candles | Number of candle records | | 44 | 4 | u32 | minutes | Number of minute records | | 48 | 4 | u32 | cells | Number of cell records | | 52 | 4 | u32 | context | Number of context records | `candles`, `minutes` and `context` are each at most 1 440, the minutes in a day. ## Candle record 52 bytes each. Current writers leave this section empty (`candles` = 0): bars are built from the minute records. Files written before 2026-10 may hold a venue's one-minute candles here; Flowscope decodes them but does not use them. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 2 | u16 | minute | Minute of the day, 0–1439 | | 2 | 1 | u8 | has_buy | `1` if `buy` is known | | 3 | 1 | u8 | — | Reserved, `0` | | 4 | 8 | f64 | open | Price | | 12 | 8 | f64 | high | Price | | 20 | 8 | f64 | low | Price | | 28 | 8 | f64 | close | Price | | 36 | 8 | f64 | volume | Base units | | 44 | 8 | f64 | buy | Taker buy volume, base units; `0` when `has_buy` is `0` | The candle opens at `day × 86 400 000 + minute × 60 000` ms. ## Minute record 140 bytes, one per minute that holds prints. Only minutes in state `Partial` or `Full` are written. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 2 | u16 | minute | Minute of the day, 0–1439 | | 2 | 1 | u8 | state | `3` = Partial, `4` = Full | | 3 | 1 | u8 | — | Reserved, `0` | | 4 | 4 | u32 | prints | Number of trades | | 8 | 4 | i32 | open | Ticks relative to `base_tick` | | 12 | 4 | i32 | high | Ticks relative to `base_tick` | | 16 | 4 | i32 | low | Ticks relative to `base_tick` | | 20 | 4 | i32 | close | Ticks relative to `base_tick` | | 24 | 8 | f64 | buy | Buy-aggressor volume, base units | | 32 | 8 | f64 | sell | Sell-aggressor volume, base units | | 40 | 4 | u32 | cells | Number of cell records belonging to this minute | | 44 | 4 | u32 | buy_trades | Number of buy trades | | 48 | 4 | u32 | sell_trades | Number of sell trades | | 52 | 44 | f32 × 11 | buy bands | Buy volume per trade-size band, base units | | 96 | 44 | f32 × 11 | sell bands | Sell volume per trade-size band, base units | Prices: `price = (base_tick + open) × tick_size`, and the same for high, low and close. The 11 bands classify each print by its quote notional: under $1K, $1K–10K, $10K–25K, $25K–50K, $50K–100K, $100K–250K, $250K–500K, $500K–1M, $1M–2.5M, $2.5M–5M, and $5M and up. Other state values exist in memory (`0` None, `1` Loading, `2` Unavailable) but are never written. A decoder rejects a minute record with any state other than 3 or 4. ## Cell record 12 bytes. One per price row with volume, grouped by minute in minute-record order: the first minute's `cells` records come first, then the next minute's, and so on. The sum of all minutes' `cells` equals the header's `cells`. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 4 | i32 | row | Row relative to `floor(base_tick / row_ticks)` | | 4 | 4 | f32 | buy | Buy-aggressor volume in the row, base units | | 8 | 4 | f32 | sell | Sell-aggressor volume in the row, base units | The absolute row is `floor(base_tick / row_ticks) + row`, using floor division. It covers the ticks `[abs_row × row_ticks, (abs_row + 1) × row_ticks)`, that is prices from `abs_row × row_ticks × tick_size` up to the next row. ## Context record 88 bytes, one per minute that holds any context. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 2 | u16 | minute | Minute of the day, 0–1439 | | 2 | 2 | u16 | — | Reserved, `0` | | 4 | 8 | f64 | oi | Last open interest seen in the minute, base units | | 12 | 4 | f32 | funding | Last funding rate seen in the minute, as a fraction | | 16 | 4 | f32 | liq_long | Longs liquidated in the minute, quote notional | | 20 | 4 | f32 | liq_short | Shorts liquidated in the minute, quote notional | | 24 | 32 | f32 × 8 | depth bid | Resting bids within ±0.1, 0.25, 0.5, 1, 2, 2.5, 5 and 10 % of the mid, base units | | 56 | 32 | f32 × 8 | depth ask | Resting asks within the same bands, base units | Depth is cumulative: the 1 % figure includes the 0.5 % one. `NaN` marks an unknown value. How far a band really reaches depends on how deep the venue publishes its book (see [Liquidity heatmap](/docs/app/heatmap#how-it-is-recorded)): beyond it the figure is what the book holds. ## Checksum The last 4 bytes are a u32 **FNV-1a 32-bit** hash of every byte before them, from the magic to the end of the last context record. ```rust fn fnv1a(bytes: &[u8]) -> u32 { bytes.iter().fold(0x811c_9dc5u32, |h, b| (h ^ u32::from(*b)).wrapping_mul(0x0100_0193)) } ``` ## File size ```text size = 56 + 52 × candles + 140 × minutes + 12 × cells + 48 × context + 4 ``` A decoder must check that the file is exactly this long. ## Decoding checklist The reference decoder rejects a segment, in this order, when: | Check | Error | |---|---| | Shorter than the version 1 header plus checksum | Truncated | | Magic is not `FSD1` | Not a Flowscope segment | | Version is not 3 | Version not supported | | Checksum does not match | Checksum mismatch | | Codec is not 0 or 1, or a compressed body does not inflate to its raw length | Corrupt: codec | | A count above 1 440, or `row_ticks` below 1 | Corrupt: counts | | Total length does not match the counts | Truncated | | A minute index of 1 440 or more, or a state other than 3 or 4 | Corrupt: minute | | The minutes' cell counts do not add up to `cells` | Corrupt: cells | | A context minute of 1 440 or more | Corrupt: context | ## Compressed {#compressed} What files and the API carry (codec `1`): the raw encoding above with everything after `version` deflated (RFC 1951): ```text "FSD1" | version u16 | codec u16 = 1 | raw length u32 | deflate(raw bytes from `day` through the raw checksum) | fnv1a-32 of everything before, u32 ``` Check the outer checksum, inflate to `raw length − 8` bytes, and put `FSD1`, the version and codec `0` in front: the result is the raw layout, with its own checksum. About 2.4× smaller than raw. ## In Rust ```rust use flowscope_engine::segment::{decode, market_key, segment_url_path, day_name, parse_day_name}; let bytes = std::fs::read("2026-10-03.fsd")?; let seg = decode(&bytes)?; println!("day {} tick {} candles {}", day_name(seg.day), seg.tick_size, seg.candles.len()); ``` `market_key` and `segment_url_path` build the names used on disk and in [`/v1/segments/`](/docs/api/segments). Source: https://flowscope.91-99-118-129.sslip.io/docs/api/segment-format --- # Positions codec > The byte layout of Hyperliquid levels and footprint payloads, as served by the API and appended to day files. Hyperliquid level runs and footprint minutes use one small binary codec, both on the wire (the answers of [`/v1/hl/levels`](/docs/api/hl-levels) and [`/v1/hl/footprint`](/docs/api/hl-footprint)) and on disk (the `.levels` and `.fp` day files Capture writes). The reference implementation is `flowscope_engine::positions::codec`. ## Principles - **Little-endian**, no padding. - **Self-delimiting**: a payload's length follows from its counts. - **Checksummed**: every payload ends with an FNV-1a 32-bit hash of its own bytes. - **Appendable**: a day file is payloads written one after another. A reader decodes payloads until the first truncated or damaged one and ignores the rest, so a crash mid-write loses only the last append. - **Versioned**: both payload types are at version `1`. ## Levels payload {#levels-payload} ```text "FSPL" u16 version u16 0 f64 bucket_px i64 latest i64 from i64 to u8 coin_len coin u32 n n × (i64 start u32 dur i32 bucket u8 kind f32 notional) u32 fnv ``` ### Header | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 4 | bytes | magic | ASCII `FSPL` | | 4 | 2 | u16 | version | `1` | | 6 | 2 | u16 | — | Reserved, `0` | | 8 | 8 | f64 | bucket_px | Price bucket size; must be positive and finite | | 16 | 8 | i64 | latest | Newest sample time the sender had, ms | | 24 | 8 | i64 | from | Start of the range answered, ms | | 32 | 8 | i64 | to | End of the range answered, ms | | 40 | 1 | u8 | coin_len | Length of the coin name, at most 255 | | 41 | coin_len | UTF-8 | coin | Hyperliquid coin name | | 41 + coin_len | 4 | u32 | n | Number of runs | In an API answer, `from` is the requested start or later when nothing is recorded before it. In a day file, `from` and `to` are the day's bounds. ### Run record 21 bytes each. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 8 | i64 | start | Start, ms since the epoch | | 8 | 4 | u32 | dur | Duration, ms. The run ends at `start + dur`. | | 12 | 4 | i32 | bucket | Prices `[bucket × bucket_px, (bucket + 1) × bucket_px)` | | 16 | 1 | u8 | kind | See below; values above 5 are rejected | | 17 | 4 | f32 | notional | Total USD notional of the orders or positions in the bucket | | Kind | Name | Meaning | |---|---|---| | 0 | LongTp | Take-profit of a long: a sell above the price | | 1 | ShortTp | Take-profit of a short: a buy below the price | | 2 | LongSl | Stop of a long: a sell below the price | | 3 | ShortSl | Stop of a short: a buy above the price | | 4 | LongLiq | Liquidation of a long, below the price | | 5 | ShortLiq | Liquidation of a short, above the price | Kinds 0–1 make up the take-profit map, 2–3 the stop-loss map and 4–5 the liquidation map. ### Trailer | Size | Type | Field | |---|---|---| | 4 | u32 | FNV-1a 32 of every byte of this payload before the trailer, from the magic on | Total size: `45 + coin_len + 21 × n + 4` bytes. ## Footprint payload {#footprint-payload} ```text "FSPF" u16 version u16 0 f64 row_px i64 latest i64 from u8 coin_len coin u32 n n × (i64 minute f32 oi mark tracked_long tracked_short volume taker_known both_known u32 cells cells × (i32 row f32 d_oi nl ns volume)) u32 fnv ``` ### Header | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 4 | bytes | magic | ASCII `FSPF` | | 4 | 2 | u16 | version | `1` | | 6 | 2 | u16 | — | Reserved, `0` | | 8 | 8 | f64 | row_px | Price row size; must be positive and finite | | 16 | 8 | i64 | latest | Newest sample time the sender had, ms | | 24 | 8 | i64 | from | Start of the range answered, ms | | 32 | 1 | u8 | coin_len | Length of the coin name | | 33 | coin_len | UTF-8 | coin | Hyperliquid coin name | | 33 + coin_len | 4 | u32 | n | Number of minutes | There is no `to`: the footprint payload covers minutes from `from` on. ### Minute record 40 bytes, followed by its cells. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 8 | i64 | minute | Minute index, `ms / 60 000` | | 8 | 4 | f32 | oi | Hyperliquid open interest at the end of the minute, coins; `NaN` if unknown | | 12 | 4 | f32 | mark | Mark price at the end of the minute; `NaN` if unknown | | 16 | 4 | f32 | tracked_long | Long size of the tracked accounts, coins; `NaN` if unknown | | 20 | 4 | f32 | tracked_short | Short size of the tracked accounts, coins; `NaN` if unknown | | 24 | 4 | f32 | volume | Traded volume in the minute, coins | | 28 | 4 | f32 | taker_known | Volume whose aggressor was tracked | | 32 | 4 | f32 | both_known | Volume with both sides tracked | | 36 | 4 | u32 | cells | Number of cell records that follow | ### Cell record 20 bytes each, directly after their minute record. | Offset | Size | Type | Field | Meaning | |---|---|---|---|---| | 0 | 4 | i32 | row | Prices `[row × row_px, (row + 1) × row_px)` | | 4 | 4 | f32 | d_oi | Δ open interest, coins (counted where both sides are tracked) | | 8 | 4 | f32 | nl | Δ net long by the aggressor, coins | | 12 | 4 | f32 | ns | Δ net short by the aggressor, coins | | 16 | 4 | f32 | volume | Traded volume in the row, coins | ### Trailer A u32 FNV-1a 32 of every byte of the payload before it, from the magic on. ## Checksum Both payload types use the same 32-bit FNV-1a as [day segments](/docs/api/segment-format#checksum): offset basis `0x811C9DC5`, prime `0x01000193`, applied byte by byte. ```rust fn fnv1a(bytes: &[u8]) -> u32 { bytes.iter().fold(0x811c_9dc5u32, |h, b| (h ^ u32::from(*b)).wrapping_mul(0x0100_0193)) } ``` ## Decoding A reader checks, per payload: | Check | Error | |---|---| | Magic is `FSPL` or `FSPF` as expected | Not a position payload | | Version is `1` | Unsupported version | | Each count fits in the remaining bytes | Truncated | | The coin name is valid UTF-8 | Corrupt: coin | | Every run kind is 0–5 | Corrupt: kind | | The trailer matches | Checksum mismatch | | `bucket_px` or `row_px` is positive and finite | Corrupt: bucket / row | To read a day file, decode payloads from offset 0 until the end of the file or the first error. Everything before the error is valid; anything after is a torn tail. ## In Rust ```rust use flowscope_engine::positions::codec::{decode_levels, decode_footprint, decode_levels_file}; // One payload from the API. let levels = decode_levels(&std::fs::read("btc.levels")?)?; let footprint = decode_footprint(&std::fs::read("eth.fp")?)?; // A day file: every intact payload and the bytes they cover. let (payloads, good) = decode_levels_file(&std::fs::read("raw/auxiliary/hl/BTC/2026-10-04.levels")?); ``` `decode_footprint_file` does the same for `.fp` files. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/positions-codec --- # Caching and CORS > Which answers of the data hub can be cached and for how long, and how browsers can call the API. The data hub sets `Cache-Control` and an `ETag` on history answers. The policy is what makes history cheap to serve to many clients: finished days and hours are final, so caches keep them. ## Cache policy | Answer | `Cache-Control` | Why | |---|---|---| | Segment of a day before yesterday (UTC) | `public, max-age=86400` | Final; a day so a rebuild after a fix reaches caches | | Segment of today or yesterday | `no-cache` | Today grows; yesterday's last minutes may still be derived after midnight | | A whole derived heatmap hour | `public, max-age=86400` | Final, served as the file | | The open hour, ranges that include it | `no-cache` | Grows with the archive | | `404 not recorded` for a segment | `no-cache` | The day may appear later | | Day lists (`/v1/segments//`) | `no-cache` | Grow every day | | `/v1/markets` | `private, max-age=10` | Changes with what Capture archives | | Everything under `/v1/hl/` and `/v1/options/` | `no-cache` | Answers end at the newest sample | | `/v1/health`, errors | none | Must reach the hub | `no-cache` allows a cache to store the answer but requires it to revalidate before reuse. ## ETag and conditional requests History answers carry an `ETag`: for a file, its trailing checksum and length; for an answer the hub merged (today's segment with the open market's minutes), a hash of the bytes sent. Send it back as `If-None-Match` and an unchanged answer is `304 Not Modified` with no body. ```bash curl -sI -H "$AUTH" $BASE/v1/segments/BinanceUsdm_BTCUSDT/2026-10-03.fsd | grep -i etag curl -s -o /dev/null -w '%{http_code}\n' -H "$AUTH" -H 'If-None-Match: "6f1c2a90-7d3e"' \ $BASE/v1/segments/BinanceUsdm_BTCUSDT/2026-10-03.fsd ``` | Feature | Supported | |---|---| | `If-None-Match` / `ETag` | Yes, on history answers | | `If-Modified-Since` / `Last-Modified` | No | | `Range` | No; the whole answer is sent | ## Compression Segments and heatmap tiles are compressed inside (DEFLATE), so they are sent as they are. Text and JSON answers are small. ## Caches in front A cache in front of the hub must keep the hub's authentication: never put a public cache that answers without a key in front of protected history. An authenticated cache that honours `Cache-Control`, with the full URL (query string included) as its key, serves final days and hours from its store and passes everything else to the hub. A client opening a week of 1-minute history then reaches the hub only for today. ## CORS {#cors} Every answer carries the hub's configured `Access-Control-Allow-Origin` (`*` unless the operator restricts it) and exposes `ETag`, `Age` and `Retry-After`. Preflight requests (`OPTIONS`) are answered for `GET` and `POST` with the `X-Api-Key`, `Authorization`, `Content-Type` and `If-None-Match` headers. > [!NOTE] > The Flowscope browser app reads the hub through its own website at `/hub`: same origin, no CORS, and the website adds the key, so the browser never holds one. A page of another site cannot use your session there. Source: https://flowscope.91-99-118-129.sslip.io/docs/api/caching-and-cors ---