---
name: ftw-flowscript
description: Write Flowscript, ftw.trade's language for strategies and indicators. Program shape on ftw.trade, types and na, series, triggers, orders, tapes (sources and swaps), enums and match, phases, libraries, contract interfaces and the std modules, exact u256 integers, trained models (regression, SVM, k-NN), invariants (risk limits), and tests. Use whenever you write or read Flowscript.
---

# Flowscript

Flowscript is a small language for trading strategies and indicators,
designed for AIs to write. A program reads time series, computes values over
them, and fires actions when its conditions turn true. It has no loops and
no mutation, so it always terminates, and it is deterministic: the same bars
give the same decisions, which is why a backtest and a live run agree.

Every compile error has a stable code (`FS0412`), names its line and column,
and suggests a fix. Draft, check, read the errors, revise.

The full language reference is references/language.md (generated from
`flowscript/docs/reference.md`). Its examples use a generic host with
`bars.close("BTC-USD", "1h")` and `paper.buy({ symbol, quote })`; on
ftw.trade, read the market as below.

## A strategy on ftw.trade

```flowscript
// params: fastLen=12, slowLen=26, size=100
const close = bars.close(market, interval);
const fast = ema(close, fastLen);
const slow = ema(close, slowLen);
metric("fast", fast);
metric("slow", slow);

when (crossover(fast, slow) && !inPosition()) { paper.buy({ quote: size }); }
when (inPosition() && crossunder(fast, slow)) { paper.sell({}); }
```

- Above the first line ftw.trade adds `const market = "<series>";`,
  `const interval = "<interval>";` and one `const` per parameter, set to
  its argument (`fastLen`, `slowLen`, `size` here). It also gives `started`
  (the unix second the run started) to a program that reads it.
- With no `uses` line a strategy gets `uses { bars: "^1", paper: "^1" }`
  (plus `sources` and `pool` when it reads them). Write your own first line
  to trade a perp: `uses { bars: "^1", funding: "^1", perp: "^1" }`.
- Intervals: <!-- generated:intervals -->
`5s`, `15s`, `1m`, `5m`, `15m`, `1h`, `4h`, `1d` (5s and 15s on a pool, built from its swaps, or on a perp, built from its recorded order book).
<!-- /generated:intervals -->

What a strategy may read and do on ftw.trade, every order and setting,
worked examples and pitfalls: references/strategy-guide.md in
`ftw-strategies` (the same text `get_strategy_guide` returns).

## Modules: what a strategy can import

A strategy's `uses` line names what it reads and does: market data
(`bars`, `pool`, `feed`, `funding`, `book`, `sources`), orders and books
(`paper`, `perp`, `lp`, `wallet`, `funds`, `husher`, `poll`), protocols
(`aave`, `morpho`, `pendle`, `derive`, …) and the `earn` router, each as
`name: "^1"`. After it, `import "std/erc20";` brings in a std contract
module and `import "mylib";` one of your libraries.

- `list_modules { kind?, chain?, search? }` lists every module with the
  line that imports it; `get_module { id }` gives one whole: its reads,
  actions, markets, the series to backtest it on, how far back its history
  goes, and an example that compiles (a ready `source` for save_strategy).
- references/modules.md is the same list as Markdown; people browse it on
  Developers → Modules, where "New strategy from this" saves an example.
- A name the uses line does not declare is refused with the line to add,
  and a mistyped one with the module it may mean.

## The language in one page

- **Values**: numbers, conditions (`true`, `false` or `na`), text,
  records `{ a: 1 }`, lists `[1, 2]`, and a series of each. Every `const`
  is a series, one value per bar; `x[1]` is the previous bar's value.
- **`na`** means "not known yet" (an indicator warming up, a division by
  zero). It spreads through arithmetic and comparisons, `!na` is `na`, and
  a trigger never fires on `na`. Test with `isNa(x)`, replace with
  `nz(x, d)`.
- **Expressions**: `+ - * / %`, comparisons (they do not chain), `&& || !`,
  `c ? a : b`, `if (c) a else b`. Durations `seconds(n)`, `minutes(n)`,
  `hours(n)` are seconds.
- **Functions**: `fn mid(a: Series<number>, b: Series<number>):
  Series<number> { return (a + b) / 2; }`, lambdas `(x) => x * 2`, and
  `fold(series, seed, (st, x) => next)`, the one way to carry state.
  Guard every fold step: `isNa(x) ? st : …`.
- **Prelude indicators**: `sma ema wma rma hma dema tema vwma vwap rsi
  stoch cci mfi obv roc momentum macd macdSignal macdHist bbUpper bbLower
  stddev zscore wilderAtr trueRange adx diPlus diMinus supertrend sar
  highest lowest rolling_sum cum linreg corr barssince valuewhen pivothigh
  pivotlow` and more (references/language.md, "The prelude").
- **Trained models**: `ml.linear ml.ridge ml.lasso ml.elasticNet
  ml.logistic ml.svm ml.svr ml.knn ml.boost ml.boostClassifier`, each `(y, [x1, x2, …], window: n,
  horizon: h, retrain: r, retrainOn: cond, …settings)`. The model learns `y`
  `horizon` bars after the predictors, refits every `retrain` rows (0: once)
  or when `retrainOn` holds, and answers a prediction per bar, the same in a
  backtest and live (references/language.md, "Trained models").
- **Market profile**: `tpo.poc tpo.vah tpo.val tpo.vpoc tpo.vpocShift
  tpo.poorHigh tpo.poorLow tpo.singleAbove tpo.singleBelow tpo.nakedPocAbove
  tpo.nakedPocBelow` and more, each `(high, low, close, volume, t, window: n,
  …)`: the last whole day's (or week's, or the forming session's) TPO
  levels, the same in a backtest and live (references/language.md, "Market
  profile").
- **Metrics**: `metric("name", value)` publishes a value per bar. Publish
  what the strategy decides on; it is how you see why it did or did not
  trade.

## Triggers

- `when (cond) { … }` fires on the bar `cond` turns true, once per crossing.
  Entries belong here.
- `while (cond) { … }` fires at every bar `cond` holds. Only for actions
  meant to repeat, with a guard that turns false (warning FS0902 catches a
  `while` that buys with no guard).
- Combinators: `crossover crossunder rising falling changed once sustained
  cooldown hysteresis every("1d") daily during`.
- An `if` inside a body is read at the bar the trigger fires.

## Orders on ftw.trade

- Spot (a pool or a feed): `paper.buy({ quote })`, `paper.buyPct({ pct })`,
  `paper.sell({})` (the whole position), `paper.sellPct({ pct })`, and
  resting `paper.buyLimit/buyStop({ quote, at })`,
  `paper.sellLimit/sellStop({ at })`.
- Perps: `perp.long({ usd })`, `perp.short({ usd })`, `perp.close({})`.
- Perp maker orders: `perp.bid({ usd, at })`, `perp.ask({ usd, at })` rest post-only at `at` (any expression) until a later bar trades through, filling at `at` with the maker fee; `perp.cancel({})` pulls them. One per line, replaced by its next order. A bid must sit under the bar's close (an ask over it) or it reverts, and the book snapshot a bar reads can be older than its close, so clamp it: `at: min(book.bid(market, interval), close * 0.9999)`. On a live project's wallets a run over its one perp rests them on the venue; a run over more markets rests them on a paper project's wallets only (starter "Maker quotes").
- Taking only what the book holds near the middle: `maxImpactBps` on `perp.long`, `perp.short`, `perp.target` and `perp.hedge` (D1645) cuts the order to what rested that near in the recorded book (live: an IOC limit that far from the live middle).
- Over more than one market: `lp.range`, `lp.close`, `perp.hedge`,
  `perp.target`, `funds.toPerp`, `funds.toWallet`.
- Protocols, directly (D1342), each named by its id with the same surface
  as every protocol of its kind. Lending markets (`aave`, `spark`,
  `compound`, `kamino`): `<id>.supply`, `<id>.withdraw`, `<id>.borrow`,
  `<id>.repay` on the market read with `<id>.supplyApy("chain:asset",
  interval)`; books `<id>.supplied()`, `<id>.borrowed()`,
  `<id>.healthFactor()` (Spark and Compound supply only). Vaults (`morpho`,
  `fluid`, `euler`, `yearn`, `sky`, `ethena`): `<id>.deposit`,
  `<id>.withdraw` in the vault read with `<id>.apy("chain:vault",
  interval)`; book `<id>.deposited()`. Euler, Yearn and Morpho also take a
  vault by its address, `base:0x…`, probed for the brand on save (D1346). One lending
  market and one vault per strategy; `get_strategy_guide` lists every market.
  Husher: `husher.toPerp`, `husher.toWallet`, landing `delayMinutes` later;
  `husher.inFlight()`. A move that cannot be done reverts with why, never
  stops the run. On a live project's wallets each protocol's live markets
  follow the books on chain (Kamino from each wallet's Solana address, Sky
  through its USDC PSM, Ethena through Curve and sUSDe's cooldown); Husher
  stays on paper.
- Fixed rates (D1344): Pendle PTs, a market named by its address
  (`pendle.impliedApy("ethereum:0x…", interval)`, `pendle.ptPrice(…)`, or a
  text parameter), `pendle.buy`, `pendle.sell`, book `pendle.held()`. Live
  through Pendle's router where the market's SY takes USDC (D1347). Pairs
  with a lender: buy the PT while its implied APY beats
  `aave.supplyApy(…)` by a margin, back to Aave when the gap closes. Build's
  `protocols` stream sends every market Pendle's factories make
  (`where: { protocol: "pendle", kind: "CreateNewMarket" }`), and `prices`
  each new value of a market's feeds (`where: { feed: "pendle-ethereum-0x…-implied-apy" }`).
- Options (D1356): Derive's options on ETH, BTC, SOL, HYPE and eight more
  underlyings, on paper and on a live project's wallets (D1423).
  `derive.index("eth", interval)`, `derive.iv7d(…)` and `derive.iv30d(…)`
  (at-the-money volatility, percent) read the underlying;
  `derive.buy({ type: "call" | "put", days, strike?, usd | units })`,
  `derive.sell({ … })` (cash-secured: a put locks its strike) and
  `derive.close({})`; books `derive.value()`, `derive.delta()`,
  `derive.positions()`. Options settle at expiry against the index. See
  the ftw-protocols skill's `references/derive.md` for strategies.
- Intents (D1343): `earn` routes an asset instead of naming a protocol.
  `earn.apy("usdc", interval)` is the best rate across the registry's live
  USDC markets (`usdc:all` adds paper-only ones, `usdc:aave,morpho` keeps
  those, `usd` is any dollar); `earn.deposit`, `earn.withdraw`,
  `earn.rebalance({ minGainPct?, costUsd? })`; books `earn.deposited()`,
  `earn.heldApy()`. Use a protocol's capability to choose the protocol, earn
  to choose the asset; not both in one strategy.
- A size is a number or a parameter **as written**; a computed size is
  refused when the strategy is saved unless the program publishes it as a
  metric (`const usd = 0 - derive.delta(); metric("hedge_usd", usd);` then
  `perp.target({ usd })`), so each step hands back the value it traded.
  Size by the book with `paper.buyPct`.
- The book: `inPosition()`, `avgEntry()`, `realizedPnl()`,
  `unrealizedPnl()`, `timeInPosition()` (seconds). On a perp they read
  `na`.

## Tapes: sources and swaps

A tape is a stream of typed events, read with `map`, `window(t, "15m")`,
`resample(t, "1h")` and the reducers `count sum mean first last distinct`.
Each event lands in the bar that contains it.
A program that reads sources says so in its `uses` line, as `uses { bars: "^1", sources: "^1", paper: "^1" }`, and a source named `f3-btc-spot` is read as `sources.f3_btc_spot()` (`-` written `_`).

```text
const calls = sources.kol_calls();            // a typed source (ftw-sources)
const s = pool.swaps();                        // every swap of the run's pool
const bought = sum(window(map(s, (e) => match (e.side) { buy => e.quote, sell => 0 }), "15m"));
metric("bought_15m", bought);
when (count(window(calls, "15m")) >= 3 && !inPosition()) { paper.buy({ quote: 100 }); }
```

A number field is a number, a boolean 1 or 0, every other kind text (JSON
as compact text); an optional field left out reads 0 or `""`. Text can be
compared (`e.token == "0x…"`) but never sizes or names an order.

## Enums, match and phases

```flowscript
enum Regime { hot, quiet, normal }
const close = bars.close(market, interval);
const vol = stddev(change(ln(close)), 24);
const regime = vol > 0.02 ? Regime.hot : (vol < 0.005 ? Regime.quiet : Regime.normal);
metric("size", match (regime) { hot => 0, quiet => 1, _ => 0.5 });
```

`match` must cover every variant or end in `_`. A sum-type source's `case`
and a swap's `side` are enums. `phases Trade { phase Flat { … } phase Long
{ … } }` is a state machine whose `when … => Phase` moves between phases
at most once a bar.

## Libraries

`save_library { name, source }` keeps `fn` definitions (and contract
interfaces) under a name; `import "name";` after the `uses` line gives a
strategy its functions. A strategy keeps the library version it was saved
with until it is saved again (`list_libraries` says who is behind).

## Contract calls and the std modules

A program can call any contract function:

```flowscript
uses { bars: "^1", usdc: ERC20("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") }
import "std/erc20";
const allowed = usdc.allowance("0x00000000000000000000000000000000000000aa", "0x91ae00000000000000000000000000000000abcd");
metric("allowed", allowed);
metric("close", bars.close(market, interval));
when (allowed < tokenUnits(100, 6)) {
  usdc.approve({ spender: "0x91ae00000000000000000000000000000000abcd", amount: tokenUnits(1000, 6) });
}
```

- `uses { v: abi("Name", "0x…", ["sig", …], { Struct: "(…)" }) }` imports
  the listed functions of one contract; `interface X = abi([...])` declares
  them without an address and `uses { t: X("0x…") }` binds it.
- Arguments are passed by name. A tuple or struct is a record with exactly
  its fields, an array a list; `as` renames an overload; `from`,
  `retries`, `backoff` and `value` are call options. A `view returns (T)`
  is read like a stream with literal arguments.
- The std modules the compiler offers:

<!-- generated:flowscript-std -->
**`import "std/erc20";`**: the ERC-20 token standard (EIP-20), as an interface a program binds to a token's address: `import "std/erc20"; uses { usdc: ERC20("0x…") }`. Parameters that the standard calls `from` are named `owner` here, because `from:` is a call option; the selector is the standard's own.

- `ERC20`: `transfer(address to, uint256 amount)`, `approve(address spender, uint256 amount)`, `transferFrom(address owner, address to, uint256 amount)`, `balanceOf(address who) view returns (uint256)`, `allowance(address owner, address spender) view returns (uint256)`, `totalSupply() view returns (uint256)`, `decimals() view returns (uint8)`
- `fn tokenUnits(amount: number, decimals: number): number`

**`import "std/erc4626";`**: the tokenized vault standard (EIP-4626), as an interface a program binds to a vault's address: `import "std/erc4626"; uses { vault: ERC4626("0x…") }`. A vault is also an ERC-20 for its shares; import "std/erc20" and bind the same address as an ERC20 to move shares.

- `ERC4626`: `deposit(uint256 assets, address receiver)`, `mint(uint256 shares, address receiver)`, `withdraw(uint256 assets, address receiver, address owner)`, `redeem(uint256 shares, address receiver, address owner)`, `asset() view returns (address)`, `totalAssets() view returns (uint256)`, `balanceOf(address who) view returns (uint256)`, `convertToAssets(uint256 shares) view returns (uint256)`, `convertToShares(uint256 assets) view returns (uint256)`, `maxWithdraw(address owner) view returns (uint256)`, `maxRedeem(address owner) view returns (uint256)`, `previewDeposit(uint256 assets) view returns (uint256)`, `previewRedeem(uint256 shares) view returns (uint256)`

**`import "std/math";`**: Exact monetary and signed-counter arithmetic. Results are decimal text, not floats; use u256Cmp/i64Cmp for ordering and explicit display conversions.

- `fn uint256(value)`
- `fn long(value)`
- `fn parseUnits(amount, decimals)`
- `fn formatUnits(amount, decimals)`
- `fn mulDiv(a, b, denominator)`
- `fn mulDivUp(a, b, denominator)`
- `fn basisPoints(amount, bps)`
- `fn basisPointsUp(amount, bps)`

**`import "std/vwap";`**: the reads a VWAP trader makes around the VWAP itself: where price sits in σ, the week's and the month's anchors, whether the day is balanced or trending, and a failed push into a band. The VWAPs are the prelude's: periodVwap (a session's, reset each period), anchoredVwap (from any bar) and their σ, periodVwapDev and anchoredVwapDev.

- `fn weekStarts(t: Series<number>): Series<bool>`
- `fn monthStarts(t: Series<number>): Series<bool>`
- `fn vwapZ(price: Series<number>, vw: Series<number>, dev: Series<number>): Series<number>`
- `fn vwapDrift(vw: Series<number>, dev: Series<number>, anchor: Series<bool>): Series<number>`
- `fn ridingAbove(close: Series<number>, vw: Series<number>, dev: Series<number>, len: number): Series<number>`
- `fn ridingBelow(close: Series<number>, vw: Series<number>, dev: Series<number>, len: number): Series<number>`
- `fn failedPushUp(high: Series<number>, open: Series<number>, close: Series<number>, upper: Series<number>, within: number): Series<bool>`
- `fn failedPushDown(low: Series<number>, open: Series<number>, close: Series<number>, lower: Series<number>, within: number): Series<bool>`

Each module has a `.test.yaml` suite beside it in `flowscript/std/`, run in CI.
<!-- /generated:flowscript-std -->

**On ftw.trade today**, a contract call compiles, and a backtest or a test
records each call with its arguments as evaluated at its bar (a test can
expect `{ fires: usdc.approve, args: { amount: 1000000000 } }`). A running
strategy does **not** send contract calls from project wallets yet: it
carries out paper, perp, lp and funds orders only. Use contract calls to
design and test; tell the person they will not execute on ftw.trade until
that is built.

## Exact integers (u256)

A number is a 64-bit float, exact only to 2^53. The `u256` functions carry
an unsigned 256-bit integer as a word of decimal digits:
`u256(x)`, `u256Add`, `u256Sub`, `u256Mul`, `u256Div`, `u256Mod`,
`u256Min`, `u256Max`, `u256Cmp(a, b)` (-1, 0, 1), `u256Units(amount,
decimals)` (exact base units, so 1.1 is eleven tenths) and
`u256Number(x, decimals)` (back to a number, for charts and thresholds,
never for an amount you send). Overflow, underflow and division by zero are
`na`, and an action with an `na` argument is refused before it is signed. A
contract-event source's `exact` field holds integers as such words.

## Invariants (risk limits)

```flowscript
const close = bars.close(market, interval);
when (crossover(close, ema(close, 20)) && !inPosition()) { paper.buy({ quote: 100 }); }
when (inPosition() && crossunder(close, ema(close, 20))) { paper.sell({}); }
invariants {
  drawdown() <= pct(5);
  dailyLoss() <= 50;
  trades.perDay() <= 4;
  on_breach: pause;
}
```

While a bound is breached (or unreadable) every entry is refused and the
refusal recorded; a sell is never refused. `flatten_and_pause` also sells
everything when a drawdown or daily-loss bound breaks. Backtests, tests and
live runs enforce the same cage; a backtest lists refusals under `paused`,
and a test expects one with `{ paused: "paper.buy", at?, by?: "drawdown" }`.

## Tests

`test_strategy { id | source, tests }` feeds a strategy bars you write and
checks what it does; saved tests (`save_strategy { tests }`) run on every
save, and `run_strategy` refuses while one fails.

```json
{ "name": "buys the step up once, never sells", "bars": 80,
  "ohlc": { "steps": [[0, 100], [30, 130]] },
  "expect": [
    { "fires": "paper.buy", "at": 30, "args": { "quote": 100 } },
    { "fills": "paper.buy", "at": 30 },
    { "fires": "paper.sell", "count": 0 } ] }
```

- Series are lists (`null` is na) or generators: `constant`, `ramp`,
  `sine`, `steps`, `walk` (seeded), each with `na: [bars]`.
- Expectations: `check` (a condition over the program's consts, at a bar or
  `ever | always | never | last`), `fires`, `fills`, `reverts`, `paused`,
  `metric … equals | na`, `warns`.
- `runs: N` repeats a case over N seeded random walks: the way to state a
  rule that must hold in any market ("never more than a fifth of equity
  in").
- A failed `fires … at` says what each part of the condition read at that
  bar.

## Diagnostics and warnings

Errors stop a save. Warnings compile but usually mean a bug: FS0901 a
condition never true (`close > highest(close, 20)`; compare with
`highest(close, 20)[1]`), FS0902 a `while` that buys every bar, FS0903 an
unused const, FS0904 a divide by literal 0, FS0905 `bps(n)` used as a number
(it is n, not n / 10000). A backtest also lists under
`quiet` each trigger that never fired and why.

## Coming from Pine Script

`get_pine_translation { pine }` translates a Pine strategy into Flowscript
and compiles it, so what comes back either compiles or says why.
