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;