---
title: Strategy Functions
description: The strategy() declaration and every strategy.* method - orders, brackets, cancellation, and position getters - with full parameter tables.
verified:
  engine: 3.0.67
  probes:
    - scripts/probes/strategies/trend-brackets.ks
---

## Overview

Functions for declaring a backtestable strategy and placing simulated orders against the broker emulator. Concepts, fill semantics, and result interpretation live in the [Strategies section](../strategies/overview.md); this page is the per-parameter reference.

| Function | Description |
| --- | --- |
| [`strategy`](#strategy) | Declare the script as a strategy and configure the simulated broker |
| [`strategy.entry`](#strategy.entry) | Queue an entry order (market, limit, or stop) |
| [`strategy.exit`](#strategy.exit) | Attach protective bracket legs (profit/loss/stop/limit/trailing) to entries |
| [`strategy.close`](#strategy.close) | Close a named entry at market |
| [`strategy.closeAll`](#strategy.closeall) | Close the entire position at market |
| [`strategy.cancel`](#strategy.cancel) | Cancel pending unfilled orders by id |
| [`strategy.cancelAll`](#strategy.cancelall) | Clear the entire pending order queue |
| [Getters](#getters) | `positionSize`, `equity`, `netProfit`, and friends |

A script contains either `define()` or `strategy()`, never both. Calling any `strategy.*` method without a `strategy()` declaration is a compile-time error (`strategy methods require a strategy() declaration`).

## strategy

`strategy(title, position?, axis?, customTitle?, format?, maxBarsBack?, initialCapital?, currency?, commissionPercent?, slippageBps?, slippageModel?, qtyType?, qtyValue?, pyramiding?, fillModel?, instrument?, leverage?, maintenanceMarginPercent?, makerFeePercent?, takerFeePercent?, funding?, onLiquidation?)`: declares the strategy and its broker configuration. Accepts everything `define()` accepts plus the broker parameters. Perps-specific parameters are since engine 3.0.63. Literal-only arguments, validated like `define()`; any broker parameter may also be wired to an `input()` so users can adjust it from script settings.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | string | required | Display name, surfaced on the chart legend and the Strategy Tester |
| `position` | `'onchart' \| 'offchart'` | `'onchart'` | Strategies default onchart so trade markers land on the price series |
| `initialCapital` | number | `10000` | Starting equity; must be positive |
| `currency` | string | `"USD"` | Display label for money-denominated stats |
| `commissionPercent` | number | `0` | Spot only: charged per fill as a percent of fill notional. Ignored under `instrument="perps"` with a diagnostic if nonzero (see [Cross-instrument parameters](#cross-instrument-parameters)) |
| `slippageBps` | number | `0` | Adverse basis points on market, stop, and trailing fills. Limit fills are exempt (a limit price is a bound) |
| `slippageModel` | `'fixed' \| 'bookEstimate'` | `'fixed'` | `'bookEstimate'` prices market-crossing fills from recorded order-book depth where available, falling back to `slippageBps` with a counted disclosure. See [Slippage and Costs](../strategies/slippage-and-costs.md) |
| `qtyType` | `'fixed' \| 'percentOfEquity' \| 'cash'` | `'percentOfEquity'` | Default sizing for entries without an explicit `qty` |
| `qtyValue` | number | `100` | Units for `'fixed'`; percent for `'percentOfEquity'` (`(qtyValue/100 * equityAtFill) / fillPrice`); account currency for `'cash'` (`qtyValue / fillPrice`). On perps, `'percentOfEquity'` and `'cash'` size committed margin, and notional is margin times leverage |
| `pyramiding` | number | `1` | Maximum stacked same-direction entries; excess entries are rejected and counted |
| `fillModel` | `'pessimistic' \| 'pathHeuristic'` | `'pessimistic'` | Intrabar ordering assumption when a bar could fill two levels and no finer data covers it. See [Fill Simulation](../strategies/fill-simulation.md) |
| `instrument` | `'spot' \| 'perps'` | `'spot'` | Broker instrument mode. `'perps'` switches to isolated-margin perpetual futures: leverage, maker/taker fees, funding, and liquidation become active. See [Perps](../strategies/perps.md) |
| `leverage` | number | `1` | Perps only. Any number literal `> 0`, fractions included (`0.5` commits twice the notional). Committed isolated margin per entry is `notional / leverage` |
| `maintenanceMarginPercent` | number | `0.5` | Perps only. Number literal `>= 0` and `< 100` (lower bound inclusive, upper exclusive); sets the liquidation threshold |
| `makerFeePercent` | number | `0` | Perps only. Charged on limit-priced fills as a percent of fill notional; number literal `>= 0` |
| `takerFeePercent` | number | `0` | Perps only. Charged on market-crossing fills as a percent of fill notional; number literal `>= 0` |
| `funding` | `'data' \| 'off'` | `'data'` | Perps only. `'data'` settles recorded funding events against open positions and counts uncovered bars; `'off'` disables funding exactly. See [Funding](../strategies/perps.md#funding) |
| `onLiquidation` | `'continue' \| 'halt'` | `'continue'` | Perps only. After a liquidation: keep trading, or reject every subsequent entry and report `liquidationHalted: true` |

**Returns:** `void`.

```javascript title="perps-happy.ks"
//@version=3

// Perps happy path: every perps param declared explicitly. Probes the declared
// echo, taker fees charged on market fills (makerFeesPaid stays 0 with no limit
// orders), commissionPercent NOT charged (it is a spot-only param and is left
// at its default 0 here so no warning fires), and the perps-only bar-aligned
// series committedMarginSeries / fundingPaidSeries being present.
// funding="off" keeps funding an exact no-op: all three funding stats are 0.
strategy(title="Perps Happy", initialCapital=10000,
         instrument="perps", leverage=5, maintenanceMarginPercent=1,
         makerFeePercent=0.02, takerFeePercent=0.05,
         funding="off", onLiquidation="continue",
         qtyType="fixed", qtyValue=1, pyramiding=1);

timeseries data = ohlcv(symbol=currentSymbol, exchange=currentExchange);

timeseries fastMa = sma(source=data.close, period=4);
timeseries slowMa = sma(source=data.close, period=9);

if (crossover(fastMa, slowMa)) {
  strategy.entry("Long", "long");
}

if (crossunder(fastMa, slowMa)) {
  strategy.close("Long");
}

plotLine(value=strategy.equity(), width=1, colors=["#2563eb"], label=["Equity"], desc=["Strategy equity"]);
```

### Validation of the perps parameters

All compile-time, literal-only, with these exact rejections (each error also carries the declaration's source location, e.g. `at 8:1`):

| Declaration | Compile error |
| --- | --- |
| `instrument="margin"` | `strategy(): 'instrument' must be 'spot' or 'perps'` |
| `instrument=1` | `strategy(): 'instrument' must be a string literal` |
| `leverage=0` / `leverage=-2` / `leverage=someIdentifier` | `strategy(): 'leverage' must be a number literal > 0` |
| `maintenanceMarginPercent=-1` / `=100` | `strategy(): 'maintenanceMarginPercent' must be a number literal >= 0 and < 100` |
| `makerFeePercent=-0.1` | `strategy(): 'makerFeePercent' must be a number literal >= 0` |
| `takerFeePercent=-0.1` | `strategy(): 'takerFeePercent' must be a number literal >= 0` |
| `funding="on"` | `strategy(): 'funding' must be 'data' or 'off'` |
| `onLiquidation="stop"` | `strategy(): 'onLiquidation' must be 'continue' or 'halt'` |

Boundary values that ARE legal: `leverage=0.5` (any number `> 0`) and `maintenanceMarginPercent=0` (the `>= 0` is inclusive) both run cleanly and echo exactly in the declaration.

Parameters can never reference script variables: `strategy()` must be the script's first statement, so a `var` placed above it fails earlier with `Script must start with a define(title, position, axis) or strategy(title) statement`, and an identifier passed to a perps param is rejected as a non-literal with the same `must be a number literal` text.

### Cross-instrument parameters

Declaring a parameter for the instrument you did not pick produces a run diagnostic (or silence), never a charge:

- Perps + nonzero `commissionPercent`: `STRATEGY_PERPS_COMMISSION_IGNORED: strategy(): 'commissionPercent' is ignored for instrument="perps"; fees come from 'makerFeePercent'/'takerFeePercent'`. Commission is not charged.
- Spot + `makerFeePercent`/`takerFeePercent`: `STRATEGY_SPOT_MAKER_TAKER_IGNORED: strategy(): 'makerFeePercent' and 'takerFeePercent' apply only to instrument="perps"; spot uses 'commissionPercent'` (the message names only the params you declared). Maker/taker are not charged; commission is.
- Spot + non-default `onLiquidation`: same diagnostic code, wording `'onLiquidation' applies only to instrument="perps"`.
- Spot + `funding="off"`, `leverage`, or `maintenanceMarginPercent`: silently accepted and echoed, but inert.

Full evidence and the run-side mechanics (margin, liquidation, funding settlement) are on the [Perps page](../strategies/perps.md).


## strategy.entry

`strategy.entry(id, direction, qty=na, limit=na, stop=na, ocaName=na, comment=na)`: queues an entry. Plain calls are market orders for the next bar open; `limit`/`stop` create resting orders that fill when touched. Re-issuing an id replaces the pending unfilled order with that id.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Your key for this entry; exits target it via `fromEntry`, and `strategy.close(id)` closes it |
| `direction` | `'long' \| 'short'` | Order direction. An entry opposite the current position is a reversal: it closes the position fully at the same fill, then opens the new one |
| `qty` | number | Optional explicit quantity; overrides the declaration's `qtyType` sizing. Fractional quantities are legal |
| `limit` | number | Optional limit price: fills when the bar trades through it, at the better of open and limit, with no slippage. Must be a number, not a timeseries: passing `data.close` directly is a compile error (`Type mismatch for 'strategy.entry.limit': expected number, got timeseries`); read it into a `var` first |
| `stop` | number | Optional stop price: fills on touch, slippage-adjusted (stops cross the market) |
| `ocaName` | string | Optional one-cancels-all group shared with other entries and exits: when any grouped order fills, the rest cancel |
| `comment` | string | Optional free text carried on the trade |

**Returns:** `void`. Rejected when the pyramiding cap is reached, sizing resolves non-positive, the bar has NaN OHLC, or the bar is not confirmed; on `instrument="perps"`, also when the fee-inclusive required margin exceeds available margin (disclosed with a `STRATEGY_MARGIN_REJECTED` diagnostic, see [Perps](../strategies/perps.md#margin-rejection)). Rejections are counted in `stats.rejectedOrders`, never thrown.

## strategy.exit

`strategy.exit(id, fromEntry=na, qty=na, qtyPercent=na, profit=na, limit=na, loss=na, stop=na, trailPoints=na, trailOffset=na, ocaName=na, comment=na)`: attaches protective bracket legs to open entries. At least one of `profit`, `limit`, `loss`, `stop`, or a trailing pair is required. Legs within one exit id are inherently one-cancels-all: a full fill cancels the siblings, a partial fill keeps the bracket armed for the remainder.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Key for this exit order; re-issuing replaces the pending version |
| `fromEntry` | string | Target entry id. Omitted: protects all open entries |
| `qty` / `qtyPercent` | number | Total contracts (or percent of targeted quantity) to exit, allocated FIFO across targeted trades |
| `profit` / `loss` | number | Take-profit / stop distance in ticks from entry; must be finite and positive |
| `limit` / `stop` | number | Absolute take-profit / stop prices; must be finite and positive. Ticks and absolute together on the same leg is a diagnostic |
| `trailPoints` | number | Favorable excursion (price units) that activates the trailing stop |
| `trailOffset` | number | Trailing distance once active; the stop ratchets monotonically with new favorable extremes |
| `ocaName` | string | Optional one-cancels-all group shared with entries and exits |
| `comment` | string | Optional free text |

**Returns:** `void`. Pre-arming before the entry fills is allowed; once armed, the exit auto-cancels if its trades are closed by another order. Take-profit limit legs fill without slippage; stop and trailing legs are slippage-adjusted. When both a stop and a profit level fall inside one bar, resolution follows [fill simulation](../strategies/fill-simulation.md#intrabar-ordering-and-fill-models).

## strategy.close

`strategy.close(id, comment=na)`: market-closes the trades opened by entry `id` at the next bar open.

## strategy.closeAll

`strategy.closeAll(comment=na)`: market-closes the entire position at the next bar open. Exit reason is `closeAll`.

## strategy.cancel

`strategy.cancel(id)`: removes all pending unfilled orders with that id across entry and exit namespaces. Unknown ids are silent no-ops.

## strategy.cancelAll

`strategy.cancelAll()`: clears the entire pending queue, including a pending `closeAll`.

## Getters

All zero-argument, valid on any bar, reflecting state as of the current bar:

| Getter | Returns |
| --- | --- |
| `strategy.positionSize()` | Signed open quantity (negative for shorts, `0` when flat) |
| `strategy.positionAvgPrice()` | Average entry price of the open position |
| `strategy.equity()` | Cash plus open position marked at the current close, minus fees paid |
| `strategy.openProfit()` | Unrealized PnL of the open position |
| `strategy.netProfit()` | Realized net profit so far |
| `strategy.closedTradeCount()` | Closed trades so far |
| `strategy.winTradeCount()` / `strategy.lossTradeCount()` | Winning / losing closed trades so far |
| `strategy.maxDrawdown()` | Largest equity decline so far |

```javascript title="trend-brackets.ks"
//@version=2
strategy(title="Trend with Brackets", initialCapital=10000, qtyType="fixed", qtyValue=1, pyramiding=1, fillModel="pathHeuristic")

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries fast = sma(source=bars.close, period=5)
timeseries slow = sma(source=bars.close, period=20)
var lastClose = bars.close

if (crossover(fast, slow)) {
  strategy.entry("Trend", "long")
}
if (strategy.positionSize() > 0) {
  strategy.exit("Protect", fromEntry="Trend", stop=lastClose * 0.97, limit=lastClose * 1.05)
}

plotLine(value=fast, width=1, colors=["#4f8cff"], label=["Fast SMA"], desc=["5-period SMA of close"])
plotLine(value=slow, width=1, colors=["#8b5cf6"], label=["Slow SMA"], desc=["20-period SMA of close"])
```

## Order lifecycle notes

- Orders placed during bar N join the queue for bar N+1; the processing sequence each bar is protective exits, then signal exits, then entries, each in insertion order.
- All order methods are recorded as rejected (not thrown) on NaN-OHLC bars and non-confirmed bars.
- On the last bar, unfilled orders remain visible under `pendingOrders` in the output.
- The position model is netting: one net position, reversals close-then-open atomically at one fill price.
