Reference
Generated from source — do not hand-edit. Rendered from backtest-engine/src/core/context.ts, backtest-engine/src/types.ts, backtest-engine/scripts/gen-strategy-dts.mjs by pnpm --filter docsite gen:reference; a test fails when it drifts.

Strategy API

These are the ambient types available inside a strategy script — the same declarations the Editor feeds to autocomplete. A strategy exports onBar(bar, ctx) (or onTick(tick, ctx) in portfolio mode) and optionally init(ctx) and config; see Strategies for the how and why. readonly state fields are omitted from the member names below but preserved in each declaration block.

StrategyContext

Member Type Description
params Record<string, ParamValue>
barIndex number
bar OHLCV | null null during init()
bars readonly OHLCV[] Bars up to and including the current bar. Live reference — it grows as the run advances; do not mutate, and copy if you need a snapshot.
position PositionView
equity number Live account equity (cash + mark-to-market of the open position); during onBar this reflects marks as of the PREVIOUS bar's close, since mark-to-market runs after onBar returns — contrast PortfolioStrategyContext.equity, which is live at the CURRENT tick's marks (deliberately not aligned; see portfolioContext.ts).
cash number
orders readonly Order[]
indicator (type: IndicatorType, params?: Record<string, number>, opts?: { timeframe?: string } ): IndicatorCursor Register indicators in init() — series are precomputed once and cached; calling inside onBar() works (lazy compute + cache hit) but re-derives the cache key per call. Pass opts.timeframe (e.g. '1h') to compute the indicator over UTC-resampled higher-timeframe bars instead of the run's base bars — see resample.ts. The returned cursor's value/at/output indirect through the no-lookahead base-bar-to-HTF-bucket mapping: null until the second HTF bucket completes, and constant across every base bar of a still-forming bucket. Throws when timeframe or the run's own base interval isn't a known interval string, or when timeframe isn't a strictly-larger exact multiple of the base interval.
buy (opts?: { qty?: number; tag?: string; expiresAfterBars?: number }): Order Place a market buy. qty defaults to 1. Fills at the NEXT bar's open.
sell (opts?: { qty?: number; tag?: string; expiresAfterBars?: number }): Order Place a market sell. qty defaults to 1. Fills at the NEXT bar's open.
limit (opts: { side: Side; qty: number; price: number; tag?: string; expiresAfterBars?: number; }): Order
stop (opts: { side: Side; qty: number; price: number; tag?: string; expiresAfterBars?: number; }): Order
stopLimit (opts: { side: Side; qty: number; stopPrice: number; limitPrice: number; tag?: string; expiresAfterBars?: number; }): Order
cancel (orderId: string): boolean
cancelAll (): void
oco (a: Order, b: Order): void Link two open orders as one-cancels-the-other: the first to fill (or, for a stop-limit, the first to trigger) cancels the other with reason 'oco-canceled'. Throws unless both orders belong to this run, are both status === 'open', and are distinct.
trailingStop (opts: { side: Side; qty: number; trailAmount?: number; trailPercent?: number; tag?: string; expiresAfterBars?: number; }): Order Place a trailing stop. Exactly one of trailAmount/trailPercent must be given (a positive finite number). Builds a type: 'stop' order with NO stopPrice; trailStopPrice initializes from the CURRENT bar's close (close − trail for sell, close + trail for buy; trail = trailAmount ?? close × trailPercent / 100) and thereafter only ratchets favorably, triggering intra-bar like a normal stop against the next bar.
bracket (opts: { side: Side; qty: number; entry?: { type: 'market' } | { type: 'limit'; price: number }; takeProfit: number; stopLoss: number; tag?: string; }): { entry: Order; takeProfit: Order; stopLoss: Order } Place an entry (market or limit) plus two attached exits — takeProfit (limit) and stopLoss (stop) — same qty, opposite side, reduce-only, OCO-linked to each other. Exits are pending (awaitingParent) until the entry fills, then become eligible from the NEXT bar. Entry defaults to a market order. Validated at placement (a BUY entry requires takeProfit > stopLoss; SELL requires takeProfit < stopLoss); on violation the entry is placed but immediately rejected (status: 'rejected', reason: 'bracket-invalid') and both exits are canceled (reason: 'parent-canceled') — nothing becomes active. All three orders are always returned. If the entry is later canceled/rejected/expired without filling, its still-awaiting exits cancel with 'parent-canceled'. Manually flattening a bracketed position (e.g. a plain ctx.sell()) does NOT cancel its still-open exits — they stay resting and armed, and cancel via 'reduce-only' only if one later triggers while it would increase/flip exposure rather than reduce it. Strategies that flatten manually should explicitly cancel the remaining exit(s) themselves; auto-cancel-on-flatten is a deliberate non-change here (design decision deferred).
crossover (a: IndicatorCursor | number, b: IndicatorCursor | number): boolean True when a crossed above b between the previous and current bar. Numeric operands are constants — crossover(x, y) with two numbers is always false. Returns false during warm-up (null values). NOTE: against higher-timeframe cursors (opts.timeframe) the compared pair holds constant across every base bar of a bucket, so the condition stays true for the WHOLE bucket after a crossing — guard entries with position checks.
size { percentOfEquity(pct: number): number; riskPerTrade(riskPct: number, stopPrice: number): number; } Pure position-sizing calculators. Neither method places an order — each returns a quantity for you to pass into buy/sell/limit/etc. Both use the CURRENT bar's close as a price proxy (the actual fill, for market orders, lands on the NEXT bar's open). Never throw; degenerate inputs simply size to 0. Both read equity, which during onBar is one-bar-stale (see the equity doc above) — PortfolioStrategyContext.size does not share this staleness; deliberately not aligned. Both are contract-multiplier aware: they return a quantity in UNITS (shares/contracts) whose MONEY value is the fraction of equity you asked for. At contractMultiplier: 50 you therefore get ~1/50th the units you would on an equity at the same price — not the same count of 50×-larger positions.
session { dayOfWeek(): number; timeOfDayMs(): number; date(): string; isFirstBarOfDay(): boolean; isLastBarOfDay(): boolean; } UTC calendar helpers derived from bar timestamps. Calendar-free — pure UTC date/time math, no exchange sessions, timezones, or DST.
crossunder (a: IndicatorCursor | number, b: IndicatorCursor | number): boolean True when a crossed below b between the previous and current bar. Numeric operands are constants — crossunder(x, y) with two numbers is always false. Returns false during warm-up (null values).
log (...args: unknown[]): void
random (): number
[key: string] unknown User code may attach its own properties (ctx.sma_fast = ...).

IndicatorType is the union of every key in the indicator catalogue. Dynamic properties you assign to ctx (e.g. ctx.fast) are typed any in the editor.

Declaration
interface StrategyContext {
	readonly params: Record<string, ParamValue>;
	readonly barIndex: number;
	readonly bar: OHLCV | null; // null during init()
	/** Bars up to and including the current bar. Live reference — it grows as the run advances; do not mutate, and copy if you need a snapshot. */
	readonly bars: readonly OHLCV[];
	readonly position: PositionView;
	/** Live account equity (cash + mark-to-market of the open position); during onBar this reflects marks as of the PREVIOUS bar's close, since mark-to-market runs after onBar returns — contrast PortfolioStrategyContext.equity, which is live at the CURRENT tick's marks (deliberately not aligned; see portfolioContext.ts). */
	readonly equity: number;
	readonly cash: number;
	readonly orders: readonly Order[];
	/**
	 * Register indicators in init() — series are precomputed once and cached;
	 * calling inside onBar() works (lazy compute + cache hit) but re-derives
	 * the cache key per call.
	 *
	 * Pass `opts.timeframe` (e.g. `'1h'`) to compute the indicator over UTC-resampled
	 * higher-timeframe bars instead of the run's base bars — see resample.ts. The
	 * returned cursor's `value`/`at`/`output` indirect through the no-lookahead
	 * base-bar-to-HTF-bucket mapping: null until the second HTF bucket completes, and
	 * constant across every base bar of a still-forming bucket. Throws when `timeframe`
	 * or the run's own base interval isn't a known interval string, or when `timeframe`
	 * isn't a strictly-larger exact multiple of the base interval.
	 */
	indicator(
		type: string,
		params?: Record<string, number>,
		opts?: { timeframe?: string }
	): IndicatorCursor;
	/** Place a market buy. qty defaults to 1. Fills at the NEXT bar's open. */
	buy(opts?: { qty?: number; tag?: string; expiresAfterBars?: number }): Order;
	/** Place a market sell. qty defaults to 1. Fills at the NEXT bar's open. */
	sell(opts?: { qty?: number; tag?: string; expiresAfterBars?: number }): Order;
	limit(opts: {
		side: Side;
		qty: number;
		price: number;
		tag?: string;
		expiresAfterBars?: number;
	}): Order;
	stop(opts: {
		side: Side;
		qty: number;
		price: number;
		tag?: string;
		expiresAfterBars?: number;
	}): Order;
	stopLimit(opts: {
		side: Side;
		qty: number;
		stopPrice: number;
		limitPrice: number;
		tag?: string;
		expiresAfterBars?: number;
	}): Order;
	cancel(orderId: string): boolean;
	cancelAll(): void;
	/**
	 * Link two open orders as one-cancels-the-other: the first to fill (or, for a
	 * stop-limit, the first to trigger) cancels the other with reason 'oco-canceled'.
	 * Throws unless both orders belong to this run, are both `status === 'open'`, and are distinct.
	 */
	oco(a: Order, b: Order): void;
	/**
	 * Place a trailing stop. Exactly one of trailAmount/trailPercent must be given (a positive
	 * finite number). Builds a `type: 'stop'` order with NO stopPrice; `trailStopPrice`
	 * initializes from the CURRENT bar's close (`close − trail` for sell, `close + trail` for
	 * buy; `trail = trailAmount ?? close × trailPercent / 100`) and thereafter only ratchets
	 * favorably, triggering intra-bar like a normal stop against the next bar.
	 */
	trailingStop(opts: {
		side: Side;
		qty: number;
		trailAmount?: number;
		trailPercent?: number;
		tag?: string;
		expiresAfterBars?: number;
	}): Order;
	/**
	 * Place an entry (market or limit) plus two attached exits — takeProfit (limit) and
	 * stopLoss (stop) — same qty, opposite side, reduce-only, OCO-linked to each other.
	 * Exits are pending (`awaitingParent`) until the entry fills, then become eligible from
	 * the NEXT bar. Entry defaults to a market order.
	 *
	 * Validated at placement (a BUY entry requires takeProfit > stopLoss; SELL requires
	 * takeProfit < stopLoss); on violation the entry is placed but immediately rejected
	 * (`status: 'rejected'`, `reason: 'bracket-invalid'`) and both exits are canceled
	 * (`reason: 'parent-canceled'`) — nothing becomes active. All three orders are always
	 * returned. If the entry is later canceled/rejected/expired without filling, its
	 * still-awaiting exits cancel with 'parent-canceled'.
	 *
	 * Manually flattening a bracketed position (e.g. a plain ctx.sell()) does NOT cancel its
	 * still-open exits — they stay resting and armed, and cancel via 'reduce-only' only if one
	 * later triggers while it would increase/flip exposure rather than reduce it. Strategies
	 * that flatten manually should explicitly cancel the remaining exit(s) themselves;
	 * auto-cancel-on-flatten is a deliberate non-change here (design decision deferred).
	 */
	bracket(opts: {
		side: Side;
		qty: number;
		entry?: { type: 'market' } | { type: 'limit'; price: number };
		takeProfit: number;
		stopLoss: number;
		tag?: string;
	}): { entry: Order; takeProfit: Order; stopLoss: Order };
	/** True when a crossed above b between the previous and current bar. Numeric operands are constants — crossover(x, y) with two numbers is always false. Returns false during warm-up (null values). NOTE: against higher-timeframe cursors (opts.timeframe) the compared pair holds constant across every base bar of a bucket, so the condition stays true for the WHOLE bucket after a crossing — guard entries with position checks. */
	crossover(a: IndicatorCursor | number, b: IndicatorCursor | number): boolean;
	/**
	 * Pure position-sizing calculators. Neither method places an order — each returns a
	 * quantity for you to pass into buy/sell/limit/etc. Both use the CURRENT bar's close as
	 * a price proxy (the actual fill, for market orders, lands on the NEXT bar's open).
	 * Never throw; degenerate inputs simply size to 0. Both read `equity`, which during onBar
	 * is one-bar-stale (see the `equity` doc above) — PortfolioStrategyContext.size does not
	 * share this staleness; deliberately not aligned.
	 *
	 * Both are contract-multiplier aware: they return a quantity in UNITS (shares/contracts)
	 * whose MONEY value is the fraction of equity you asked for. At `contractMultiplier: 50`
	 * you therefore get ~1/50th the units you would on an equity at the same price — not the
	 * same count of 50×-larger positions.
	 */
	readonly size: {
		/**
		 * qty = floor((equity * pct / 100) / (close * contractMultiplier)). Returns 0 when
		 * pct, equity, or close is non-finite or <= 0, or when the computed qty is not a
		 * positive finite number.
		 */
		percentOfEquity(pct: number): number;
		/**
		 * qty = floor((equity * riskPct / 100) / (|close - stopPrice| * contractMultiplier)) —
		 * the stop distance is converted from points to money per unit through the multiplier
		 * before dividing. Returns 0 on any degenerate input: riskPct non-finite or <= 0,
		 * equity non-finite or <= 0, or a zero/non-finite close-to-stop distance.
		 */
		riskPerTrade(riskPct: number, stopPrice: number): number;
	};
	/**
	 * UTC calendar helpers derived from bar timestamps. Calendar-free — pure UTC date/time
	 * math, no exchange sessions, timezones, or DST.
	 */
	readonly session: {
		/** UTC day of week of the current bar (0 = Sunday, …, 6 = Saturday). */
		dayOfWeek(): number;
		/** Milliseconds since UTC midnight for the current bar. */
		timeOfDayMs(): number;
		/** UTC date of the current bar, `yyyy-mm-dd`. */
		date(): string;
		/** True when there is no previous bar, or the previous bar falls on a different UTC date. */
		isFirstBarOfDay(): boolean;
		/**
		 * True when there is no NEXT bar, or the next bar falls on a different UTC date.
		 *
		 * LOOKAHEAD WARNING: this reads the NEXT bar's TIMESTAMP ONLY (never its price) to
		 * detect the day change — it does not use any future price/volume data. This is a
		 * documented, intentional lookahead: a standard backtest convenience so strategies can
		 * flatten/exit before the session ends (e.g. `if (ctx.session.isLastBarOfDay()) ctx.sell()`).
		 * Do not infer anything about the next bar's price from its return value.
		 */
		isLastBarOfDay(): boolean;
	};
	/** True when a crossed below b between the previous and current bar. Numeric operands are constants — crossunder(x, y) with two numbers is always false. Returns false during warm-up (null values). */
	crossunder(a: IndicatorCursor | number, b: IndicatorCursor | number): boolean;
	log(...args: unknown[]): void;
	random(): number;
	/** User code may attach its own properties (ctx.sma_fast = ...). */
	[key: string]: unknown;
}

IndicatorCursor

Member Type Description
value number | null Primary output at the current bar ('values' key if present, else first output).
outputs Record<string, number | null> All outputs at the current bar.
at (lookback: number): number | null Primary output lookback bars ago (0 = current).
output (name: string, lookback?: number): number | null Named output at the current bar or lookback bars ago.
Declaration
interface IndicatorCursor {
	/** Primary output at the current bar ('values' key if present, else first output). */
	readonly value: number | null;
	/** All outputs at the current bar. */
	readonly outputs: Record<string, number | null>;
	/** Primary output `lookback` bars ago (0 = current). */
	at(lookback: number): number | null;
	/** Named output at the current bar or `lookback` bars ago. */
	output(name: string, lookback?: number): number | null;
}

PositionView

Member Type Description
qty number absolute size
avgPrice number
isLong boolean
isShort boolean
isFlat boolean
unrealizedPnl number Mark-to-market P&L of the open position in MONEY, not points: (currentClose - avgPrice) * signedQty * contractMultiplier. 0 when flat. On an instrument with a multiplier (ES = 50, an equity option = 100) this is the dollar figure, so a threshold like unrealizedPnl < -500 means $500.
Declaration
interface PositionView {
	readonly qty: number; // absolute size
	readonly avgPrice: number;
	readonly isLong: boolean;
	readonly isShort: boolean;
	readonly isFlat: boolean;
	/**
	 * Mark-to-market P&L of the open position in MONEY, not points:
	 * `(currentClose - avgPrice) * signedQty * contractMultiplier`. 0 when flat. On an
	 * instrument with a multiplier (ES = 50, an equity option = 100) this is the dollar
	 * figure, so a threshold like `unrealizedPnl < -500` means $500.
	 */
	readonly unrealizedPnl: number;
}

OHLCV

Bar shape — structurally identical to basecomponents' OHLCV.

Member Type Description
timestamp number
open number
high number
low number
close number
volume number
partial? boolean True when bar is built from incomplete data (gap-fill couldn't fully cover)
Declaration
/** Bar shape — structurally identical to basecomponents' OHLCV. */
interface OHLCV {
	timestamp: number;
	open: number;
	high: number;
	low: number;
	close: number;
	volume: number;
	/** True when bar is built from incomplete data (gap-fill couldn't fully cover) */
	partial?: boolean;
}

Side

Declaration
type Side = 'buy' | 'sell';

OrderType

Declaration
type OrderType = 'market' | 'limit' | 'stop' | 'stop-limit';

OrderStatus

Declaration
type OrderStatus = 'open' | 'filled' | 'canceled' | 'rejected';

OrderSpec

Member Type Description
side Side
qty number
type OrderType
limitPrice? number
stopPrice? number
tag? string Optional caller tag echoed back on the order (for strategy bookkeeping).
expiresAfterBars? number Resting bars before auto-cancel (time-in-force). Must be a positive finite number. In portfolio runs (see runPortfolioBacktest, whose strategy entry point is onTick), this counts MERGED-AXIS TICKS, not the order's own symbol's bars — an order on a sparsely-traded symbol can expire during a gap before that symbol trades again.
ocoWith? string Sibling order id for one-cancels-other linking (set via ctx.oco). First fill or stop-limit trigger cancels the sibling.
trailAmount? number Trailing stop distance in absolute price units. Exactly one of trailAmount/trailPercent may be set. Trailing stops are type: 'stop' with one of these fields and NO stopPrice — place via ctx.trailingStop(), not ctx.stop().
trailPercent? number Trailing stop distance as a percent of price (e.g. 5 = 5%). Exactly one of trailAmount/trailPercent may be set. See trailAmount.
parentId? string Entry order id this order is an attached exit for (set via ctx.bracket()).
reduceOnly? boolean When true, a fill that would increase or flip exposure cancels instead of executing (reason 'reduce-only'). Set on bracket exits via ctx.bracket().
Declaration
interface OrderSpec {
	side: Side;
	qty: number;
	type: OrderType;
	limitPrice?: number;
	stopPrice?: number;
	/** Optional caller tag echoed back on the order (for strategy bookkeeping). */
	tag?: string;
	/**
	 * Resting bars before auto-cancel (time-in-force). Must be a positive finite number. In
	 * portfolio runs (see runPortfolioBacktest, whose strategy entry point is onTick), this
	 * counts MERGED-AXIS TICKS, not the order's own symbol's bars — an order on a
	 * sparsely-traded symbol can expire during a gap before that symbol trades again.
	 */
	expiresAfterBars?: number;
	/** Sibling order id for one-cancels-other linking (set via ctx.oco). First fill or stop-limit trigger cancels the sibling. */
	ocoWith?: string;
	/**
	 * Trailing stop distance in absolute price units. Exactly one of trailAmount/trailPercent
	 * may be set. Trailing stops are `type: 'stop'` with one of these fields and NO stopPrice —
	 * place via ctx.trailingStop(), not ctx.stop().
	 */
	trailAmount?: number;
	/**
	 * Trailing stop distance as a percent of price (e.g. 5 = 5%). Exactly one of
	 * trailAmount/trailPercent may be set. See trailAmount.
	 */
	trailPercent?: number;
	/** Entry order id this order is an attached exit for (set via ctx.bracket()). */
	parentId?: string;
	/**
	 * When true, a fill that would increase or flip exposure cancels instead of executing
	 * (reason 'reduce-only'). Set on bracket exits via ctx.bracket().
	 */
	reduceOnly?: boolean;
}

Order

Member Type Description
id string
spec OrderSpec
status OrderStatus
placedAtBar number Bar index during whose onBar() the order was placed. Active from placedAtBar + 1. In portfolio runs (see runPortfolioBacktest, whose strategy entry point is onTick), this is the MERGED-AXIS TICK index (onTick's tick argument), not any single symbol's own bar index — PortfolioBroker.place/processTick gate and evaluate orders against that shared tick axis.
expiresAtBar? number Bar index at which an unfilled resting order auto-cancels (set from spec.expiresAfterBars at placement).
filledAtBar? number
fillPrice? number
reason? string Set when status is 'canceled' or 'rejected'.
triggered? boolean Internal: a stop-limit whose stop has triggered now rests as a limit.
trailStopPrice? number Current ratcheted trailing-stop level (trailing stops only). Set at placement, then only ever moves favorably.
awaitingParent? boolean True while a bracket exit is pending its parent entry's fill. Inactive (never fills/expires/ratchets) until cleared on the entry's fill.
Declaration
interface Order {
	id: string;
	spec: OrderSpec;
	status: OrderStatus;
	/**
	 * Bar index during whose onBar() the order was placed. Active from placedAtBar + 1. In
	 * portfolio runs (see runPortfolioBacktest, whose strategy entry point is onTick), this is
	 * the MERGED-AXIS TICK index (onTick's `tick` argument), not any single symbol's own bar
	 * index — PortfolioBroker.place/processTick gate and evaluate orders against that shared
	 * tick axis.
	 */
	placedAtBar: number;
	/** Bar index at which an unfilled resting order auto-cancels (set from spec.expiresAfterBars at placement). */
	expiresAtBar?: number;
	filledAtBar?: number;
	fillPrice?: number;
	/** Set when status is 'canceled' or 'rejected'. */
	reason?: string;
	/** Internal: a stop-limit whose stop has triggered now rests as a limit. */
	triggered?: boolean;
	/** Current ratcheted trailing-stop level (trailing stops only). Set at placement, then only ever moves favorably. */
	trailStopPrice?: number;
	/** True while a bracket exit is pending its parent entry's fill. Inactive (never fills/expires/ratchets) until cleared on the entry's fill. */
	awaitingParent?: boolean;
}

ParamValue

Declaration
type ParamValue = number | string | boolean;