Strategies Overview

A strategy is an Indicator that places orders. The file declares strategy({ ... }) beside its outputs and calls strategy.long, strategy.exit, strategy.closeAll…

A strategy is an Indicator that places orders. The file declares strategy({ ... }) beside its outputs and calls strategy.long, strategy.exit, strategy.closeAll and the position getters from ./gen/strategy inside its bar logic; the host runs the same broker the kScript (legacy) Strategy Tester runs, so a kScript strategy ports one to one as one Indicator file, and its trade list, equity curve and stats come out bit for bit the same wherever the engine is deterministic. This page is the map: what a strategy file is, what a run returns, the guarantees, and where the twelve kScript strategy pages land in this tree.

What a strategy file is

The smallest useful one: long when the fast EMA crosses above the slow one, flat when it crosses back under.

// Moving average cross as a strategy: long when the fast EMA crosses above the slow one, flat when it crosses back under.
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, Ema } from "./sdk/ta";

strategy({ initialCapital: 10000, qtyType: "percentOfEquity", qtyValue: 50, commissionPercent: 0.05, slippageBps: 2 });
input("close", ohlcv.close);
output("fast", line, overlay, { description: "9-period EMA of close" });
output("slow", line, overlay, { description: "21-period EMA of close" });

const fastEma = new Ema(9);
const slowEma = new Ema(21);
const cross = new Cross();
let fast: f64 = NaN;
let slow: f64 = NaN;
let crossed: i32 = 0;

export function init(): void {}

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

export function finalize(): void {
  if (crossed == 1) strategy.long("L").send();
  if (crossed == -1) strategy.closeAll();
  out_fast(fast);
  out_slow(slow);
  emitRow();
}

export function reset(): void {
  fastEma.reset();
  slowEma.reset();
  cross.reset();
  fast = NaN;
  slow = NaN;
  crossed = 0;
}

Three things make it a strategy rather than an indicator:

  • strategy({ ... }) at the top level replaces kScript's strategy(...) line. It is the broker: starting equity, sizing (50% of equity per entry), commission and slippage live in the file, so a shared package carries its own assumptions. Every field is optional; a bare strategy() takes every default. The build derives a strategy section into the sheet and marks the package a strategy.
  • strategy.long("L").send() in finalize() queues a market order under the id L. An order placed on a bar fills no earlier than the next bar's open; nothing in a run can act on information it did not have.
  • strategy.closeAll() is the signal exit. Orders go in finalize() only; the position getters (strategy.positionSize() and the rest) answer in state() and finalize() and read the position after the bar's fills.

The outputs stay outputs: fast and slow are metric ids after install, chartable and alertable like any Indicator's. The strategy adds two host-published outputs of its own, strategy.position (the signed position after each bar's fills) and strategy.equity, readable as wrun/@you/ma-cross/strategy.position by screens, threshold alerts and the metric rules.

What a run returns

Every host that runs the package (a chart, an alert, om backtest) carries the engine's own result beside the outputs: a trades list (entries, exits, per-trade PnL, fees, exit reason), an equity curve and drawdown series, performance stats (net profit, win rate, profit factor, Sharpe, Sortino, max drawdown, exposure and the rest), and the open trades and pending orders as of the last bar. On a chart that is the Strategy Tester panel with trade markers on the price series; in the terminal it is the backtest card, and the JSON report carries the engine's records verbatim under strategy.

om wrun install ./ma-cross
om backtest @you/ma-cross --asset BINANCE_FUTURES:BTCUSDT --window 90d

om backtest @scope/name names an installed package (add @version to pin one), --asset names the market the strategy trades (the broker fills against that market's candles, which is why a strategy's price input carries no market pin), and the window replays over whole closed bars. The card's header reads fill=engine_broker with the package's own commission and slippage; passing --fee-bps, --slippage-bps, --latency-bars or a book size is refused by name, because costs live in the file. Reading the Strategy Tester walks the card.

Core guarantees

  • Deterministic. The same package on the same bars produces the same trades, on every host: the broker is the engine's own code, run under the engine's own phase order, with no data providers attached.
  • Lookahead-free. Orders placed while bar N computes fill no earlier than bar N+1's open. The position a guard reads in state() is the position after that bar's fills, so a flat guard is already flat on the bar its exit filled.
  • Honest accounting. A bar that could have filled two levels settles by the declared fillModel and counts in ambiguousFillCount; rejected orders (the pyramiding cap, a size the equity cannot fund, an order placed on a live chart's forming bar) are counted, never dropped. The run details say bar resolution: this release attaches no finer-interval data, no order book and no funding data.

What the broker is, and is not

The same research tool the kScript tester is: fast iteration and honest reporting over bar closes. It settles orders at the next open, resting limits and stops when touched, protective exits under the fill model, isolated margin, liquidation and maker or taker fees on perps. It does not model queue position, partial fills at a level, replenishment, latency, or venue fees beyond the declared commission or maker and taker rates, and in this release it does not walk finer bars, price fills off a recorded book (slippageModel: "bookEstimate" is refused by name) or settle recorded funding (funding: "data" counts the bars it could not settle). Each is named in the run's stats rather than approximated.

The rule lane, beside this one

An Indicator that computes values and places no orders still backtests: a rule over one of its metric ids, authored as a candidate's condition source (a tree with the strategy's on_true/on_false as the sides, or a band that names its own sides) and replayed by om backtest spec --candidate-file through the replay venue, with --fee-bps and --slippage-bps as flags and a saved watch as the paper path. That lane keeps its own fill model (fill=next_bar_open) and its own report; the one-file form on these pages is the path for anything with ids, brackets, partial exits or a broker to reproduce.

Section contents