Strategies
A strategy is a TypeScript module that reacts to bars and places orders through ctx. This page
walks through the module shape, fill/equity timing, indicators, orders, sizing, sessions,
parameters, portfolio mode, and going live.
Module shape
A strategy exports onBar(bar, ctx) — called once per bar — or, in portfolio mode,
onTick(tick, ctx) instead; a module needs at least one of the two. Both config (your
parameter defaults) and init(ctx) (called once before the run starts) are optional. See the
Strategy API reference for the full ctx member list
and every field's exact type.
Lifecycle and timing
init(ctx) runs once, before bar 0 — ctx.bar is null for the whole call, since there's no
current bar yet. Register indicators here so the engine precomputes and caches their series
once instead of on every bar.
Fills happen at the next bar's open. ctx.buy()/ctx.sell() are market orders that fill at
the next bar's open, never the same bar — there are no same-bar fills, by design. One
consequence: ctx.equity during onBar reflects marks as of the previous bar's close, not the
bar you're currently on — mark-to-market runs after onBar returns, so equity is always one bar
stale while you're deciding. Sizing helpers that read ctx.equity (below) inherit this.
Indicators
Register with ctx.indicator(type, params?, opts?) —
call it in init() so the series precomputes once and caches; calling it inside onBar() still
works (cache hit) but re-derives the cache key on every call. The returned cursor exposes
.value (the primary output at the current bar), .at(lookback) (primary output N bars back),
and .output(name, lookback?) (a named output for multi-output indicators). Every built-in type
is listed in the indicator catalogue; your own saved indicator scripts
are callable the same way, by the lower-cased-alphanumeric slug of their name (an indicator named
"My Osc" is ctx.indicator('myosc')).
Pass opts.timeframe (e.g. '1h') to compute over UTC-resampled higher-timeframe bars instead
of the run's base bars. This is strictly no-lookahead: the cursor stays null until the second
HTF bucket completes, and holds constant across every base bar of a still-forming bucket — so
an ctx.crossover/ctx.crossunder condition against an HTF cursor stays true for the whole
bucket after it fires, not just the bar it happened on. Guard entries with a position check, the
way the example below guards on ctx.position.isFlat.
Orders
ctx.buy/ctx.sell (market, qty defaults to 1), ctx.limit, ctx.stop, ctx.stopLimit,
ctx.trailingStop (ratchets favorably off exactly one of trailAmount/trailPercent),
ctx.bracket (entry plus OCO-linked takeProfit/stopLoss exits), ctx.oco(a, b) (links two
already-open orders), and ctx.cancel(orderId)/ctx.cancelAll(). buy, sell, limit, stop,
stopLimit, and trailingStop all accept expiresAfterBars — a time-in-force that auto-cancels
a still-resting order after N bars (bracket's three orders don't take one directly).
ctx.bracket validates its prices at placement (a BUY entry needs takeProfit > stopLoss, SELL
the reverse); an invalid bracket rejects the entry and cancels both exits instead of arming them.
Fine print: manually flattening a bracketed position (a plain ctx.sell(), say) does not
cancel that bracket's still-open exits — they stay resting and armed, and only cancel if one
later triggers while it would increase or flip exposure rather than reduce it. If you flatten by
hand, cancel the remaining exit yourself. See Fine Print for this and
other edge cases worth knowing.
export function init(ctx: StrategyContext) {
ctx.rsi = ctx.indicator('rsi', { period: 14 });
ctx.atr = ctx.indicator('atr', { period: 14 });
}
export function onBar(bar: OHLCV, ctx: StrategyContext) {
// Flatten before the UTC day rolls over; brackets don't cancel themselves on a manual exit.
if (ctx.session.isLastBarOfDay() && !ctx.position.isFlat) {
ctx.cancelAll();
ctx.sell({ qty: ctx.position.qty });
return;
}
const rsi = ctx.rsi.value;
const atr = ctx.atr.value;
if (rsi === null || atr === null || !ctx.position.isFlat) return;
if (rsi < 30) {
const stop = bar.close - 2 * atr;
ctx.bracket({
side: 'buy',
qty: Math.max(1, Math.floor(ctx.size.riskPerTrade(1, stop))),
takeProfit: bar.close + 3 * atr,
stopLoss: stop
});
}
}
Sizing and session helpers
ctx.size.percentOfEquity(pct) and ctx.size.riskPerTrade(riskPct, stopPrice) are pure
calculators — neither places an order, each just returns a quantity you pass into buy/sell/
limit/etc. Both price off the current bar's close and read ctx.equity (one-bar-stale during
onBar, as above); degenerate inputs (non-finite, zero, negative) size to 0 rather than
throwing.
Both are contract-multiplier aware. They convert money into a quantity, so they divide by the
money value of one unit, not by the raw price: percentOfEquity divides by
close × contractMultiplier and riskPerTrade divides by
|close − stopPrice| × contractMultiplier. What comes back is a count of units (shares or
contracts) whose money value is the fraction of equity you asked for. On ES at 5000 with a
multiplier of 50 and $1,000,000 of equity, ctx.size.percentOfEquity(95) returns 3 — three
contracts are the $950,000 of notional you asked for. The same call on a $5,000 stock returns
190. Nothing changes at the default multiplier of 1.
ctx.position.unrealizedPnl is money on the same terms — it is
(close − avgPrice) × qty × contractMultiplier, not a point count. On ES one contract ten points
offside reads -500, not -10, so a threshold like if (ctx.position.unrealizedPnl < -500) means
$500 on every instrument. Set the multiplier in Configure → Costs & capital; see
Fine Print for what it does and does not scale.
ctx.session is UTC-only, calendar-free math derived from bar timestamps — no exchange
sessions, timezones, or DST: dayOfWeek(), timeOfDayMs(), date(), isFirstBarOfDay(),
isLastBarOfDay(). isLastBarOfDay() peeks at the next bar's timestamp only (never its price)
to detect a day rollover — a documented, intentional lookahead so a strategy can flatten before
the session ends, exactly as the example above does.
Parameters
Declare a strategy's own parameters in config.params, keyed by name with their default values
(as in the example above). Configure's Optimization group renders one row per declared
parameter, seeded with those defaults, so you can sweep them without touching code — see
Parameters and sweeps. Inside the
script, ctx.params is typed loosely — Record<string, number | string | boolean> — individual
parameters aren't narrowed to whatever type you gave them in config.params.
Portfolio mode
With 2+ symbols, a strategy can export onTick(tick, ctx) instead of onBar — fired once per
distinct timestamp across all symbols, with every ctx call taking an explicit symbol:
export function onTick(tick, ctx) {
for (const symbol of tick.symbols) {
const bar = ctx.bar(symbol);
if (bar && ctx.position(symbol).isFlat) {
ctx.buy({ symbol, qty: 1 });
}
}
}
Portfolio types (PortfolioTick, the portfolio ctx) are not yet in the generated reference;
the Editor types them loosely. See Portfolio Mode for shared buying
power, per-symbol caps, and what portfolio runs don't support yet.
Deploying live
A saved strategy can also run as a live (or paper) deployment instead of a backtest. Deployments are browser-attended — they execute in your open tab and stop when you close it, rather than running unattended on a server — with paper as the default and a typed confirmation plus risk limits required to arm live trading. See Paper and live for the full model.