---
title: "Writing Strategies"
description: "The strategy({ ... }) declaration, the order API from ./gen/strategy, the position model and sizing. The semantics are the kScript (legacy) engine's own: the…"
order: 80
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 from `./gen/strategy`, the position model and sizing. The semantics are the kScript (legacy) engine's own: the host runs the engine's broker, so every rule below is the rule the Strategy Tester applies.

## The declaration

`strategy(options)` is one top-level statement of `src/indicator.ts`, 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, so users adjust it from the package's settings with the declaration as the default.

| 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, with a warning if nonzero. |
| `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 in this release (no order book is attached). |
| `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, with a warning if nonzero. |
| `funding` | `funding` | `"data"` | `"off"` is an exact no-op; `"data"` settles recorded funding when a provider is attached, and counts the bars it cannot settle otherwise (this release attaches none). |
| `onLiquidation` | `on_liquidation` | `"continue"` | `"halt"` rejects every entry after the first liquidation. |

The build derives the section into the sheet with the field names in the second column, and the package's title on the wire is its display name. A strategy package's price input reads `ohlcv` on the market it is run on, with no symbol or exchange pin: the broker fills against that market's own candles. The names `strategy.position` and `strategy.equity` are reserved for the host's own outputs. 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 host, so finish one order before starting the next. An unset leg is absent, the engine's `na`. There is no `comment`: the engine keeps it on no record.
- Getters, valid in `state()` and `finalize()`: `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
// 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.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_fast, out_slow } from "./gen/outputs";
import { strategy } from "./gen/strategy";
import { Cross, Sma } from "./sdk/ta";

strategy({ initialCapital: 10000, qtyType: "fixed", qtyValue: 1, pyramiding: 1, fillModel: "pathHeuristic" });
input("close", ohlcv.close);
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();
let fast: f64 = NaN;
let slow: f64 = NaN;
let close: f64 = NaN;
let crossed: i32 = 0;

export function init(): void {}

export function state(): i32 {
  close = in_close();
  fast = fastSma.update(close);
  slow = slowSma.update(close);
  if (isNaN(fast) || isNaN(slow)) return 0;
  crossed = cross.update(fast, slow);
  return 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  fastSma.reset();
  slowSma.reset();
  cross.reset();
  fast = NaN;
  slow = NaN;
  close = NaN;
  crossed = 0;
}
```

## 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 output 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 and leverage](perps-and-leverage.md) 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 (the newest bar the host holds). On the last bar, unfilled orders remain visible under pending orders in the output.

## 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.
