---
title: "Writing strategies"
description: "The strategy({ ... }) declaration, the order API, the position model and sizing. The chart's Strategy Tester runs the engine's broker, so every rule below is…"
order: 122
section: "strategies"
---

<!-- source: docs/indicators/strategies/writing-strategies.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Writing strategies

The `strategy({ ... })` declaration, the order API, the position model and sizing. The chart's Strategy Tester runs the engine's broker, so every rule below is the rule it applies.

## The declaration

`strategy(options)` is one top-level statement of the file, beside `param`, `input` and `output`. It takes the engine's setting names; every field is optional and `strategy()` enables the broker with every default. A numeric field may be the handle `param(...)` returns: the setting then shows as a number field in the strategy's settings dialog, starting at the param's default, and changing it reruns the strategy without a recompile.

| Setting | Sheet field | Default | Meaning |
| --- | --- | --- | --- |
| `initialCapital` | `initial_capital` | 10000 | Starting equity, more than 0. |
| `currency` | `currency` | `"USD"` | Display label for money-denominated stats. |
| `commissionPercent` | `commission_percent` | 0 | Spot commission on every fill, percent of notional. Ignored on perps. |
| `slippageBps` | `slippage_bps` | 0 | Adverse basis points on market, stop and trailing fills. Limit fills are exempt: a limit price is a bound. |
| `slippageModel` | `slippage_model` | `"fixed"` | `"bookEstimate"` is refused by name when the strategy runs (the chart attaches no order book). |
| `qtyType`, `qtyValue` | `qty_type`, `qty_value` | `"percentOfEquity"`, 100 | Default sizing. `"fixed"`: `qtyValue` units per entry. `"percentOfEquity"`: `qtyValue / 100 * equityAtFill / fillPrice`. `"cash"`: `qtyValue / fillPrice`. On perps the last two size margin, and notional is margin times leverage. |
| `pyramiding` | `pyramiding` | 1 | Maximum stacked same-direction entries; excess entries are rejected and counted. |
| `fillModel` | `fill_model` | `"pessimistic"` | Intrabar ordering assumption when a bar could fill two levels; see [fill simulation](fill-simulation.md). |
| `instrument` | `instrument` | `"spot"` | `"perps"` enables isolated leveraged margin, liquidation, maker and taker fees and funding. |
| `leverage` | `leverage` | 1 | Perps leverage, more than 0. |
| `maintenanceMarginPercent` | `maintenance_margin_percent` | 0.5 | Perps maintenance margin, at least 0 and under 100. |
| `makerFeePercent`, `takerFeePercent` | `maker_fee_percent`, `taker_fee_percent` | 0, 0 | Perps fee rates, percent of notional; maker on limit-bound fills, taker on market-crossing fills. Ignored on spot. |
| `funding` | `funding` | `"data"` | `"off"` is an exact no-op; `"data"` settles recorded funding when funding data is attached, and counts the bars it cannot settle otherwise (the chart attaches none). |
| `onLiquidation` | `on_liquidation` | `"continue"` | `"halt"` rejects every entry after the first liquidation. |

Run derives the section into the sheet with the field names in the second column, and checks every value with the engine's rules as the file builds: a value out of range stops the build, with the problem in the editor's **Console**. A strategy's price input reads `ohlcv` on the chart's own market, with no symbol or exchange pin (a file with no `input` line gets the chart's close as that input): the broker fills against the chart's own candles, and any other primary input is refused by name. The names `strategy.position` and `strategy.equity` are reserved for the strategy's own alert choices ("Strategy position" and "Strategy equity"): an output with either name is refused by name. There is no `calc_on_every_tick`: live behavior is fixed, [fill simulation](fill-simulation.md) explains it.

## The order API

Every call maps to one engine broker call, with the engine's own validation:

```text
strategy.long(id).qty(n).limit(px).stop(px).oca(name).send()      // strategy.entry(id, "long", ...)
strategy.short(id).qty(n).limit(px).stop(px).oca(name).send()     // strategy.entry(id, "short", ...)
strategy.exit(id).from(entryId).qty(n).qtyPercent(p)
                 .profit(pts).limit(px).loss(pts).stop(px)
                 .trail(points, offset).oca(name).send()          // strategy.exit(id, fromEntry, ...)
strategy.close(id)     strategy.closeAll()                        // market, next open
strategy.cancel(id)    strategy.cancelAll()                       // pending unfilled orders
```

- A plain `strategy.long(id).send()` is a market order for the next bar's open. `limit` or `stop` makes it a resting order that fills when touched; both on one entry is rejected and counted. Re-issuing an id replaces the pending unfilled order with that id.
- `strategy.exit(id)` attaches bracket legs to an entry: `profit` and `loss` in price points, `limit` and `stop` as absolute prices, at least one leg. Without `from` it protects every open entry. Stop, limit and trail legs under one exit id are one-cancels-all.
- `trail(points, offset)` activates a trailing stop once the trade's favorable excursion reaches `points`, then ratchets with new extremes at `offset` behind them.
- `oca(name)` joins any orders, entries and exits alike, into a one-cancels-all group: when one fills, the rest cancel.
- The builders are preallocated singletons: `strategy.long`, `strategy.short` and `strategy.exit` each reset one builder, the setters fill its legs, and `send()` is the only call that reaches the broker, so finish one order before starting the next. An unset leg is absent. There is no `comment`: the engine keeps it on no record.
- Getters, valid in `onBar()` beside the order calls: `strategy.positionSize()` (signed), `strategy.positionAvgPrice()`, `strategy.equity()`, `strategy.openProfit()`, `strategy.netProfit()`, `strategy.closedTradeCount()`, `strategy.winTradeCount()`, `strategy.lossTradeCount()`, `strategy.maxDrawdown()`.

A crossover entry bracketed by both a stop and a take-profit limit; whichever the market touches first closes the trade and cancels the other side:

```typescript sample=strat-trend-brackets
// Trend entries with bracket exits: a stop and a take-profit limit on one exit id, whichever the market touches first closes the trade and cancels the other.
strategy({ initialCapital: 10000, qtyType: "fixed", qtyValue: 1, pyramiding: 1, fillModel: "pathHeuristic" });
output("fast", line, overlay, { description: "5-period SMA of close" });
output("slow", line, overlay, { description: "20-period SMA of close" });

const fastSma = new Sma(5);
const slowSma = new Sma(20);
const cross = new Cross();

function onBar(): void {
  const close = bar.close();
  const fast = fastSma.update(close);
  const slow = slowSma.update(close);
  if (isNaN(fast) || isNaN(slow)) return;
  const crossed = cross.update(fast, slow);
  if (crossed == 1) strategy.long("Trend").send();
  if (strategy.positionSize() > 0) strategy.exit("Protect").from("Trend").stop(close * 0.97).limit(close * 1.05).send();
  out_fast(fast);
  out_slow(slow);
}
```

## Position model

The broker nets to one position. An entry in the opposite direction is a reversal: it closes the existing position fully at the same fill, then opens the new one at the requested quantity. `pyramiding` caps same-direction stacking; rejected entries are counted in the broker's `rejectedOrders` rather than thrown.

Sizing details worth knowing:

- An explicit `qty(...)` on an order always wins over the declaration's `qtyType`.
- Sized-at-fill quantities (`percentOfEquity`, `cash`) resolve from the slippage-adjusted fill price, and fractional quantities are legal (the broker does not round lots).
- On perps, `percentOfEquity` and `cash` size the isolated margin commitment, not the notional. Explicit quantities and `qtyType: "fixed"` stay direct quantities and are margin-checked. Perps entries also pass an isolated-margin admission check before the opening leg applies; unrealized PnL is not collateral. [Perps](perps.md#margin) has the model.
- A computed size is rejected and counted when equity at fill is not positive or the quantity is not finite and positive.

Order calls are recorded as rejected, not thrown, when the current bar has non-finite prices or is not confirmed (a live chart's forming bar). On the last bar, unfilled orders remain visible under **Pending orders** in the Strategy Tester's Trades tab.

## Limits

At most 64 distinct order ids per run (`closeAll` counts as one; ids are slots, re-issuing one replaces its pending order), 64 bytes of UTF-8 per id, 4096 strategy calls per bar, and 10000 closed trades per run. Each breach refuses the run by name, and every engine rejection stays a counted rejection; the [limits](../reference/limits.md) page lists them.
