Strategies

Writing Strategies

The strategy() declaration, the strategy.* order API, position rules, and sizing.

Since engine 3.0.61; verified against engine 3.0.61. Every example on this page is an executed probe against that build.

The strategy() declaration

strategy() accepts everything define() accepts (title, position, axis, customTitle, format, maxBarsBack) plus the broker parameters below. All are optional except title. Arguments are literal-only and validated like define(); any strategy param may also be wired to a normal input() so users can adjust it from the script settings, with the declaration acting as the default.

strategy(title="MA Cross",
         initialCapital=10000,        // starting equity, > 0
         currency="USD",              // display label
         commissionPercent=0.0,       // per fill, percent of notional
         slippageBps=0.0,             // adverse, per market-crossing fill
         slippageModel="fixed",       // "fixed" | "bookEstimate"
         qtyType="percentOfEquity",   // "fixed" | "percentOfEquity" | "cash"
         qtyValue=100,                // meaning depends on qtyType
         pyramiding=1,                // max stacked same-direction entries
         fillModel="pessimistic")     // "pessimistic" | "pathHeuristic"
ParameterDefaultMeaning
initialCapital10000Starting account equity. Must be positive.
currency"USD"Display label for money-denominated stats.
commissionPercent0Charged on every fill as a percent of fill notional.
slippageBps0Adverse basis points applied to market, stop, and trailing fills. Limit fills are exempt: a limit price is a bound and can fill better but never worse.
slippageModel"fixed""bookEstimate" prices market-crossing fills from recorded order-book depth where available. See Slippage and costs.
qtyType / qtyValue"percentOfEquity" / 100Default order sizing. "fixed": qtyValue units per entry. "percentOfEquity": (qtyValue / 100 * equityAtFill) / fillPrice. "cash": qtyValue / fillPrice.
pyramiding1Maximum stacked same-direction entries. Excess entries are rejected and counted, never silently dropped.
fillModel"pessimistic"Intrabar ordering assumption when a bar could fill two levels. See Fill simulation.

Strategy scripts default to position="onchart" so trade markers land on the price series.

Order API

strategy.entry(id, direction, qty=na, limit=na, stop=na, ocaName=na, comment=na)
strategy.close(id, comment=na)          // close the named entry (market, next open)
strategy.closeAll(comment=na)
strategy.cancel(id)                     // cancel pending unfilled orders with this id
strategy.cancelAll()
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)
  • direction is "long" or "short". id is your string key; re-issuing an id replaces the pending unfilled order with that id.
  • A plain strategy.entry is a market order for the next bar open. Adding limit or stop creates a resting order that fills when touched.
  • strategy.exit attaches bracket legs to an entry id: profit and loss in ticks, limit and stop as absolute prices. At least one leg is required. exit without fromEntry protects all open entries. Stop, limit, and trail legs within one exit id are inherently one-cancels-all.
  • trailPoints activates a trailing stop once the trade's favorable excursion reaches that many price units; trailOffset sets the trailing distance. The trail ratchets monotonically with new favorable extremes.
  • ocaName joins any orders (entries and exits) into a one-cancels-all group: when one fills, the rest cancel immediately.
  • Getters, valid on any bar: strategy.positionSize() (signed), strategy.positionAvgPrice(), strategy.equity(), strategy.openProfit(), strategy.netProfit(), strategy.closedTradeCount(), strategy.winTradeCount(), strategy.lossTradeCount(), strategy.maxDrawdown().
scripts/probes/strategies/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"])

Position model

The broker nets to one position. A strategy.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:

  • 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).
  • A computed size is rejected and counted when equity at fill is not positive or the quantity is not finite and positive.

Order methods are recorded as rejected (not thrown) when the current bar has NaN OHLC values or is not confirmed. On the last bar, unfilled orders remain visible under pending orders in the output.