Quantum Strategy API
Overview
A strategy is a JavaScript class that extends Strategy. It runs in a sandbox inside Quantum: modern JavaScript works (class, extends, arrow functions, destructuring, template strings), but there is no file, network or timer access and no require/import. What a strategy can use is Strategy, console, the standard built-ins, and four names Quantum provides: market, broker, log and params.
The same script runs in two places:
- Backtests walk your stored ticks or stored minute bars and fill orders in a simulator against the prints that actually happened.
- Charts: a strategy armed on a chart during a replay session trades the replay simulator. Arming re-reads the file, so an edit is picked up the next time you arm it.
A backtest can never place a real order
Workflow
- Open Strategies in Quantum. The library groups the bundled strategies by setup — trend, order flow, order book, structure, market profile — and each one is a readable example of the house style.
- Write or edit in the Editor. A file is compiled before it is saved, and one that does not compile is refused with the error and its line number.
- Run it: choose tick tapes or stored bars, the market, the dates and the settings. Start with a few days, read the statistics and the log, then widen the sample.
- Review the run in the Analyzer, which keeps every run so you can compare them. Record what you learned — including what did not work — in the file's header comment.
A strategy's shape
A complete strategy. The first line files it in the library; the header comment says what the rule is and what is known about it; the class declares its settings and answers one question per closed bar.
// @group trend
// Delta continuation -- go with a bar that closed strong on real buying.
//
// THE RULE
// On a bar that closes in its top fifth with positive delta, go long, stop
// under the bar's low. Mirror for shorts. Built for NQ on 5-minute bars.
//
// Research tool, not advice. Test it on Sim or Playback.
class DeltaContinuation extends Strategy {
static get label() { return "Delta continuation" }
static get barSeconds() { return 300 }
static get params() {
return {
minCloseLocation: { def: 0.8, lo: 0.5, hi: 1.0, real: true, group: "Entry",
desc: "How near the bar's extreme it must close. 0.8 = the top fifth." },
stopTicks: { def: 40, lo: 4, hi: 400, group: "Stops",
desc: "Fallback stop when the bar gives no usable level." },
targetTicks: { def: 80, lo: 0, hi: 800, group: "Targets",
desc: "Profit target. 0 means no target." },
qty: { def: 1, lo: 1, hi: 20, group: "Size", desc: "Contracts per entry." },
}
}
/** Consulted only while flat. 1 long, -1 short, 0 nothing. */
signal() {
const closedBar = market.bar(0)
if (closedBar.close === undefined) return 0 // no bar yet
if (closedBar.deltaFromBar) return 0 // delta missing, not zero
const closeLocation = market.closeLocation(0) // NaN on a doji
if (closeLocation >= params.minCloseLocation && closedBar.delta > 0) return 1
if (closeLocation <= 1 - params.minCloseLocation && closedBar.delta < 0) return -1
return 0
}
/** The protective stop, as a PRICE. */
suggestedStop(isLong) {
const closedBar = market.bar(0)
return isLong ? closedBar.low - market.tickSize : closedBar.high + market.tickSize
}
}
DeltaContinuationThe last line must be the class name, on its own
Statics Quantum reads
| Static | Meaning |
|---|---|
| static get label() | Display name. Defaults to the class name. |
| static get params() | Parameter declarations — they become the settings panel. |
| static get barSeconds() | The bar the rule is written for (60, 300, 900 …). Used when a backtest does not name one. 0 or absent = any. |
| static get group() | Library category. Read from the source text, so it must be a literal string; a first line of // @group trend does the same. |
Lifecycle
| Method | When it is called |
|---|---|
| signal() | Once per closed bar, only while flat. Return 1 long, −1 short, 0 nothing. The base turns it into a bracketed market order. The usual override. |
| suggestedStop(isLong) | At entry: the stop as a price, or NaN to fall back to stopTicks. |
| targetTicks(isLong) | At entry: ticks to the target. Defaults to the targetTicks parameter (0 = none). |
| onBar() | Every closed bar, flat or not. Override it to manage a position or to track state; call super.onBar() to keep signal() consulted. |
| onTick() | Every print — about 2.3 million a day — in a backtest on tick tapes only. Not called on stored bars or on a chart. Backtests get much slower. |
| init() | Once per instance: once per day in a backtest (each day gets a new instance), and on a chart each time it is armed or its settings change. Reset per-session state here. |
| maintain() | Before onBar() every bar: applies the inherited break-even and trailing stop. Override with maintain() {} to switch both off. |
State you keep on this survives from bar to bar within a run. A backtest builds a fresh instance each day, so do not rely on this across days — the value areas and prior-day levels in market do carry over. The base class also gives you this.ticksTo(price), this.trailStop(price) (which only ever moves the stop in the trade's favor) and this.breakEven(points, offsetTicks).
Parameters
static get params() {
return {
stackLevels: { def: 3, lo: 2, hi: 20, group: "Entry", desc: "Why this matters." },
riskReward: { def: 2.0, lo: 0.5, hi: 10, real: true, group: "Targets", desc: "..." },
tradeShorts: { def: 1, lo: 0, hi: 1, group: "Entry", desc: "0/1 toggles render as switches." },
}
}| Field | Meaning |
|---|---|
| def | Default. Required, numeric. |
| lo, hi | Bounds; values are clamped into them. hi < lo is refused as a typo. |
| real | true for a decimal field; otherwise whole numbers. |
| label | Control label; defaults to the key. |
| desc | Shown under the control. Say why, not what. |
| group | Panel section: Entry, Stops, Targets, Size, Diagnostics, or your own. |
| hidden | true hides it. With lo == hi == def it is pinned. |
Parameters are all numbers — use 0/1 for switches. Read them as params.stopTicks. Every strategy also inherits the stop management parameters below; restating one in your own params only changes its default. The base reads the conventional keys stopTicks, targetTicks and qty.
The five names
| Name | What it is |
|---|---|
| market | Everything you READ: bars, price, the studies, the footprint, market structure, the profiles, the order book. |
| broker | Everything you DO: buy, sell, flatten, cancelAll, moveStop, and position. |
| log | log("…") itself, plus log.warn, .error, .debug and .plot. |
| params | What static get params() declared, under the name it declared. |
| context | All of it in one object, for when that is what you mean — the same object as this.c. |
signal() {
const squeeze = market.indicators.ttmSqueeze({ length: params.squeezeLength })
if (squeeze.squeezeOn > 0) return 0 // compressed: stand aside
const ladder = market.footprint(0)
if (!ladder.cells) return 0 // this bar has no footprint
log("delta " + ladder.delta + " at " + market.price)
return ladder.delta > 0 ? 1 : -1
}Older strategies use this.c (the same object as market) and this.p (the same as params), with short names such as c.fp(n) for market.footprint(n), c.ms for market.structure and c.tpo(n) for market.marketProfile(n). They still work; new code should use the spelled-out names.
There is no bare c
this.c works; c on its own is a ReferenceError. The four names above are the only ones available without this., and a helper written outside the class that takes c as a parameter is fine — one that reaches for it is not. An audit of 72 strategies found this exact mistake breaking three of them on the first bar of every run.Reading the market
Bars are indexed by barsAgo, as in NinjaScript: 0 is the bar that just closed — never the forming one. Anything out of range returns an empty object, not zeros, so a missing value reads as undefined.
Guard every read
if (bar.close === undefined) return 0, if (!priorDay.high) return 0. A comparison against undefined is silently false, and a zero that “is a price” would pass everything — which is why nothing here returns 0 for “missing”.Bars and state
| Read | Returns |
|---|---|
| market.price, .bid, .ask | Last trade and the inside market at this print. |
| market.tickSize | Minimum price increment (0.25 on NQ and ES). |
| market.barIndex | Index of the bar being reported. |
| market.bar(n) | open high low close time volume trades delta deltaMin deltaMax cvd deltaFromBar. time is the bar's open in ms. deltaFromBar true means delta is missing, not balanced. |
| market.bar1m(n) | A closed 1-minute bar, for finer checks on a slower chart. |
| market.closeLocation(n) | 0 at the low, 1 at the high; NaN on a zero-range bar. |
| market.atr(period) | Average true range over the last period bars (a simple mean, in price); 0 until there are enough bars. |
| market.toTicks(distance) | A price distance in ticks. |
| broker.position | qty (signed), avg, and long / short / flat. |
Footprint
market.footprint(n) is a bar's ladder: poc total delta deltaMin deltaMax, and cells[] ascending by price — {price, bid, ask, delta, buyImb, sellImb, stacked, thin, buyAbs, sellAbs}. stacks[] lists same-side imbalance runs. The chart's own per-bar signals come with it: unfinished highs and lows, exhaustion, an absorption score, big buy and sell levels, the bar's value area and where its POC sits.
// one run of six or more buy imbalances -- not two runs of three
const deepBuyStack = market.footprint(0).stacks.some(run => run.isBuy && run.levels >= 6)
// the chart's own stacked-imbalance arrow for this bar: 1, -1 or 0
const arrow = market.stackedImbalance(0, { minLevels: 3, closePercent: 0.8, priorBars: 2 })Selectivity matters more than the signal: a stacked run of three or more appears on a large share of ordinary bars. Check how often a gate fires before trusting what follows it.
Market structure
| Read | Returns |
|---|---|
| dir | 1 bullish, −1 bearish, 0 undecided. |
| bosNow, chochNow | 1 / −1 / 0: a break of structure or change of character fired on THIS bar. |
| bos[] | Every break so far, with the zone that caused it and whether price has since closed through it (a breaker). |
| lastBreak | {isBullish, isChoch, price}. |
| pivots[] | Every confirmed swing: price, bar, strength (8 major, 5 minor). |
| strongHigh, strongLow | The range, and the chart's names for it. |
| fib50, fib618, fibExt | Fib levels of the range; −1 when not valid. |
| zones[] | Supply and demand zones off the swings, with whether each has broken. |
market.structure2m, structure15m and structure1h give the same read on higher timeframes, as of their last closed bar.
const structure = market.structure
if (structure.dir === undefined) return 0 // not enough bars yet
// Test the EVENT, not a level: bosNow / chochNow are non-zero only on the bar
// that broke. A level is present on that bar and a hundred bars later.
if (structure.bosNow === 1 && !structure.lastBreak.isChoch) {
log("bullish break of structure at " + structure.lastBreak.price)
}
// The 15-minute read, as of its last CLOSED bar. Its bar numbers are 15-minute
// bars, so compare with structure15m.bar, never with market.barIndex.
const higherTimeframe = market.structure15m
if (higherTimeframe.dir === 1) { /* 15-minute structure is bullish */ }Key levels
market.priorDay(n) is a completed session — 0 is yesterday: high low open close range, the initial balance (ibHigh ibLow ibMid), the opening range (orHigh orLow) and the premarket (pmHigh pmLow). market.today() is the session in progress, and market.priorWeek(n) the last completed weeks. All are resolved by time, so they never show a session that has not finished. In a backtest they are built from the days the run walks, so the first day of a run has no prior day — start a run a day before the first day you care about.
signal() {
const priorDay = market.priorDay(0) // yesterday's completed session
if (!priorDay.high) return 0 // no completed session yet
const closedBar = market.bar(0)
// Fade the first close back inside after a probe above yesterday's high.
if (closedBar.high > priorDay.high && closedBar.close < priorDay.high) return -1
return 0
}Value areas and market profile
| Read | Returns |
|---|---|
| market.marketProfile(n) | A completed RTH session's TPO profile (30-minute periods, 70% value area): poc vah val ibHigh ibLow high low. Write market-profile rules such as the 80% rule against this one. |
| market.marketProfile(n, desk) | The same, for one auction: "asia", "london" or "newyork". n counts that desk's own sessions. |
| market.valueArea(n) | A completed session's volume profile: poc vah val volume. |
| market.valueArea(n, desk) | The volume profile of one desk. Can answer { pending: true } the first time — see below. |
| market.measuredMove(n, seconds) | The impulse, the fib that triggered it, the −23.6% target, and whether targetHit or stopHit has happened. 0 seconds is this strategy's own series. |
| market.ssl(n) | The SSL channel: line (a natural stop) and state (1 green, −1 red). ssl1m and ssl5m read the 1- and 5-minute channels. |
| market.momentum(n) | Simple Momentum — the Heikin-Ashi SuperTrend drawn as arrows. signal is the flip itself (1, −1 or 0), trend the state on every bar. { raw: true } drops the EMA and SSL gates. |
// The 80% rule is written against the MARKET PROFILE (TPO), not volume.
const profile = market.marketProfile(0) // yesterday's RTH TPO profile
if (!profile.vah) return 0 // no such session
const volume = market.valueArea(0) // yesterday's VOLUME profile
log("TPO value " + profile.val + "-" + profile.vah + ", POC " + profile.poc +
" | volume POC " + volume.poc + " | IB " + profile.ibLow + "-" + profile.ibHigh)The TPO profile also reports the shape of each end — buyingTail and sellingTail as { low, high } price bands, poorHigh / poorLow for an extreme left without excess, and singlePrints for the mid-profile gaps. The class reference has every field, the desk windows, and the measured-move fields.
A desk volume area can answer “not yet”
market.valueArea(n, "asia") is the one read that can return { pending: true }: the desk volume cut is a second walk of the tick log and asking is what starts it. Test for poc, not for emptiness — pending means “not yet” and an empty map means “no such session”. In a backtest neither is ever pending.Timeframes are named
Anything that reads another timeframe takes one by name: "chart" (the series the strategy runs on), "1m", "2m", "3m", "5m", "10m", "15m", "30m", "1h", "2h", "4h", "1d" — market.timeframes lists them. A name this run cannot serve throws with the reason rather than reading as nothing.
The one exception is market.measuredMove(n, seconds), which takes a number of seconds — 0 for this strategy's own series, 300 for 5 minutes, 900 for 15 — because the named set could not answer “5 minute” at all. It returns an empty map for a series this run cannot serve, rather than throwing.
market.bar(0, "15m") // or market.bar15m(0)
market.footprint(0, "5m") // or market.footprint5m(0)
market.ssl(0, "5m") // chart, 1m and 5m only
market.structureAt("15m") // the same object as market.structure15m
market.fairValueGaps("15m", { unfilledOnly: true })
market.indicators.rsi({ period: 14 }, 0, "15m") // settings, barsAgo, timeframe
// barsAgo counts bars of THAT timeframe, and every bar number a higher
// timeframe reports is in its own bars — compare with its .bar, not barIndex.
market.timeframes // every name this run can serve183 indicators
The full list — every parameter, default and plot, with an example for each — is in the searchable indicator catalog.
Every indicator in Quantum's library is a named function on market.indicators — trend, momentum, volatility, volume and structure studies. Plots come back in their own spelling and in camelCase, and parameter names ignore case.
const kama = market.indicators.kama({ period: 20 }).kama
const squeeze = market.indicators.ttmSqueeze({ length: 20 }) // every plot
const rsi15m = market.indicators.rsi({ period: 14 }, 0, "15m") // another timeframe
const hammer = market.indicators.candlestickPattern({ Pattern: "Hammer" }) // choices by name
const fast = market.indicators.ema({ period: 9 }).ema
// Nothing fails quietly: a typo in a parameter is refused by name.
market.indicators.choppinessIndex({ Perod: 14 })
// TypeError: ChoppinessIndex has no parameter "Perod". It takes: PeriodEach function also describes itself: market.indicators.ttmSqueeze.params and .plots. Warm-up values come back undefined, never 0.
Order book
market.book.available first, always. The book exposes the same stats strip the DOM shows — book ratio, liquidity, imbalance, trade rate, absorption and more — through market.book.stats(), .stat(key) and .series(key, n).
There is no order book in a backtest
Orders and stops
Usually you never place an order: return 1 or −1 from signal() and the base places a market order with a protective bracket. To manage a trade yourself, use broker.buy / broker.sell (always with stopTicks > 0 — an entry without a stop is refused), broker.flatten(), broker.cancelAll() and this.trailStop(price). There is no pyramiding: an entry is refused while a position is open.
// Usually you never call these -- return 1 or -1 from signal() and the base
// places a bracketed market order. When you manage a trade yourself:
onBar() {
if (broker.position.long && market.bar(0).close < this.exitLevel) {
broker.flatten() // close at market, drop the bracket
}
super.onBar() // keep consulting signal() while flat
}
// broker.buy({ stopTicks: 20, targetTicks: 40, qty: 1 }) -- ALWAYS give a stopStop management every strategy inherits
| Parameter (default) | Meaning |
|---|---|
| autoTrail (1) | Walk the resting stop behind a trail source. |
| trailSource (0) | 0 this chart's SSL, 1 the 1-minute SSL, 2 the 5-minute SSL, 3 ATR behind price. |
| trailSlackTicks (12) | How far behind the SSL line the stop sits. |
| atrTrailMult (2.0), atrTrailPeriod (14) | For trail source 3. |
| breakEvenPoints (20) | Move the stop to entry once this many points ahead; 0 disables. |
| breakEvenOffsetTicks (2) | Ticks beyond entry for break-even. |
For a rule that must hold its stop, pin them off — autoTrail: { def: 0, lo: 0, hi: 0, hidden: true } — or override maintain() {}.
Logging
log(text), log.warn and log.error write to the run's log, stamped with the bar time in New York (log.debug lines are kept only after log.level("debug"), which belongs in init()). log.plot(name, value) records up to eight named series against bar time. Log every entry and every veto with the numbers that decided it: a run with no trades says nothing about why, and the log is how you debug a rule you cannot watch. Never log from onTick().
Backtesting
| Option | Meaning |
|---|---|
| Data: tick tapes | Every print with the bid and ask, so order flow is real. Only the days you downloaded for replay. Use it for order-flow rules. |
| Data: stored bars | Recorded 1-minute bars walked as a price path — about 90 days per contract. Volume and delta are estimates, and there is no footprint or order book. Use it for bar and level rules. |
| Bar period | The bar onBar is called for; defaults to the strategy's barSeconds. |
| RTH only | Only call the strategy 09:30–16:00 ET (series still build overnight). |
| Slippage | One tick per side by default — the honest floor. |
| Settings | Override any parameter for a single run. |
A run reports trades, win rate, profit factor, drawdown, average MAE and MFE, and a t-statistic with its standard error — read that before the net. Each trade lists entry, exit, ticks, MAE, MFE, bars held and why it closed (target, stop, signal or end of session).
Hypothetical results have limits
Sample strategies
Two complete strategies in the house style. Both were compiled and run in Quantum before being published here. Paste one into a new strategy in the Strategies editor to run it or change it.
onBar(), not signal(), because signal() is skipped while a position is open and would miss every bar held in a trade.// @group trend
// EMA crossover -- go with the fast average crossing the slow one.
//
// THE RULE
// When the fast EMA crosses above the slow EMA on a closed bar, go long.
// When it crosses below, go short. The stop sits beyond the recent swing
// and the target is a multiple of that risk. Built for NQ on 5-minute bars.
//
// WHY THE CROSS, NOT THE STATE
// "fast > slow" is true on every bar of a trend. The cross -- the sign of
// (fast - slow) changing -- is the event, so that is what is tested.
//
// WHAT IS KNOWN
// A documentation sample. Untested; record results here as they come in.
//
// Research tool, not advice. Test it on Sim or Playback.
class EmaCrossover extends Strategy {
/** @returns {string} what the library and the logs call it */
static get label() { return "EMA crossover" }
/** @returns {number} the bar this rule is written for, in seconds */
static get barSeconds() { return 300 }
/** @returns {Object} parameter declarations; these become the settings panel */
static get params() {
return {
fastPeriod: { def: 9, lo: 2, hi: 100, group: "Entry",
desc: "The fast average. Shorter reacts sooner and whipsaws more." },
slowPeriod: { def: 21, lo: 3, hi: 400, group: "Entry",
desc: "The slow average the fast one has to cross." },
tradeShorts: { def: 1, lo: 0, hi: 1, group: "Entry",
desc: "0 takes longs only." },
swingBars: { def: 5, lo: 1, hi: 50, group: "Stops",
desc: "How many closed bars back to look for the swing the stop sits beyond." },
stopTicks: { def: 40, lo: 4, hi: 400, group: "Stops",
desc: "Fallback stop when there are not enough bars to find a swing." },
riskReward: { def: 2.0, lo: 0.5, hi: 10, real: true, group: "Targets",
desc: "Target as a multiple of the distance to the stop." },
qty: { def: 1, lo: 1, hi: 20, group: "Size", desc: "Contracts per entry." },
}
}
/** Once per day, before the walk: forget yesterday's averages. */
init() {
this.previousSpread = undefined
this.crossDirection = 0
}
/**
* Every closed bar, flat or not. The cross is found here rather than in
* signal(), because signal() is skipped while a position is open and would
* miss every bar held in a trade.
*/
onBar() {
const fast = market.indicators.ema({ period: params.fastPeriod }).ema
const slow = market.indicators.ema({ period: params.slowPeriod }).ema
this.crossDirection = 0
if (fast !== undefined && slow !== undefined) { // undefined while warming up
const spread = fast - slow
if (this.previousSpread !== undefined) {
if (this.previousSpread <= 0 && spread > 0) this.crossDirection = 1
if (this.previousSpread >= 0 && spread < 0) this.crossDirection = -1
}
this.previousSpread = spread
}
super.onBar() // still consult signal() while flat
}
/** Consulted only while flat: act on a cross that happened on this bar. */
signal() {
if (this.crossDirection === 1) {
log("EMA " + params.fastPeriod + " crossed above " + params.slowPeriod + " at " + market.price)
return 1
}
if (this.crossDirection === -1 && params.tradeShorts) {
log("EMA " + params.fastPeriod + " crossed below " + params.slowPeriod + " at " + market.price)
return -1
}
return 0
}
/**
* The stop, as a PRICE: one tick beyond the lowest low (long) or highest
* high (short) of the last few closed bars.
* @param {boolean} isLong
* @returns {number}
*/
suggestedStop(isLong) {
let swingLow = Infinity, swingHigh = -Infinity
for (let barsAgo = 0; barsAgo < params.swingBars; ++barsAgo) {
const closedBar = market.bar(barsAgo)
if (closedBar.close === undefined) return NaN // not enough bars: use stopTicks
swingLow = Math.min(swingLow, closedBar.low)
swingHigh = Math.max(swingHigh, closedBar.high)
}
return isLong ? swingLow - market.tickSize : swingHigh + market.tickSize
}
/**
* Target in ticks: the stop distance times riskReward.
* @param {boolean} isLong
* @returns {number}
*/
targetTicks(isLong) {
const stopPrice = this.suggestedStop(isLong)
const riskTicks = isNaN(stopPrice)
? params.stopTicks
: market.toTicks(Math.abs(market.price - stopPrice))
return Math.max(1, Math.round(riskTicks * params.riskReward))
}
}
EmaCrossover
market.structure.bosNow starts the wait; the retest triggers the entry.// @group structure
// BOS retest -- after a break of structure, buy the pullback to the level.
//
// THE RULE
// When a bullish break of structure fires, remember the level it broke.
// If price pulls back to that level within a few bars and the bar closes
// back above it, go long, stop under that bar. Mirror for bearish breaks.
// Built for NQ on 5-minute bars.
//
// WHY THE EVENT, NOT THE LEVEL
// market.structure.bosNow fires on the bar that broke. A structure level is
// present on the bar that made it and a hundred bars later, so the event is
// what starts the wait, and the retest is what triggers the entry.
//
// WHAT IS KNOWN
// A documentation sample. Untested; record results here as they come in.
//
// Research tool, not advice. Test it on Sim or Playback.
class BosRetest extends Strategy {
/** @returns {string} */
static get label() { return "BOS retest" }
/** @returns {number} */
static get barSeconds() { return 300 }
/** @returns {Object} */
static get params() {
return {
retestBars: { def: 12, lo: 1, hi: 100, group: "Entry",
desc: "How many bars after the break a retest still counts." },
toleranceTicks: { def: 4, lo: 0, hi: 40, group: "Entry",
desc: "How close to the broken level the pullback must come." },
tradeShorts: { def: 1, lo: 0, hi: 1, group: "Entry", desc: "0 takes longs only." },
stopTicks: { def: 40, lo: 4, hi: 400, group: "Stops",
desc: "Fallback stop when the retest bar gives no usable level." },
riskReward: { def: 2.0, lo: 0.5, hi: 10, real: true, group: "Targets",
desc: "Target as a multiple of the distance to the stop." },
qty: { def: 1, lo: 1, hi: 20, group: "Size", desc: "Contracts per entry." },
}
}
/** Once per day: no break is waiting for a retest yet. */
init() {
this.pendingBreak = null
}
/**
* Every closed bar: start waiting when a break fires, and stop waiting when
* the retest window runs out. Done here so a break that fires while a
* position is open is not missed.
*/
onBar() {
const structure = market.structure
if (structure.bosNow === 1 || structure.bosNow === -1) {
const lastBreak = structure.lastBreak
if (lastBreak && lastBreak.price !== undefined && !lastBreak.isChoch) {
this.pendingBreak = {
isLong: structure.bosNow === 1,
level: lastBreak.price,
barIndex: market.barIndex,
}
log((this.pendingBreak.isLong ? "bullish" : "bearish") + " BOS at " + lastBreak.price + ", waiting for a retest")
}
}
if (this.pendingBreak && market.barIndex - this.pendingBreak.barIndex > params.retestBars) {
log("no retest of " + this.pendingBreak.level + " within " + params.retestBars + " bars")
this.pendingBreak = null
}
super.onBar()
}
/** Consulted only while flat: enter on a bar that retests and holds the level. */
signal() {
const pending = this.pendingBreak
if (!pending || market.barIndex === pending.barIndex) return 0 // not on the breaking bar itself
const closedBar = market.bar(0)
if (closedBar.close === undefined) return 0
const tolerance = params.toleranceTicks * market.tickSize
if (pending.isLong && closedBar.low <= pending.level + tolerance && closedBar.close > pending.level) {
log("retest held above " + pending.level)
this.pendingBreak = null
return 1
}
if (!pending.isLong && params.tradeShorts
&& closedBar.high >= pending.level - tolerance && closedBar.close < pending.level) {
log("retest held below " + pending.level)
this.pendingBreak = null
return -1
}
return 0
}
/**
* The stop, as a PRICE: one tick beyond the retest bar.
* @param {boolean} isLong
* @returns {number}
*/
suggestedStop(isLong) {
const closedBar = market.bar(0)
if (closedBar.close === undefined) return NaN
return isLong ? closedBar.low - market.tickSize : closedBar.high + market.tickSize
}
/**
* Target in ticks: the stop distance times riskReward.
* @param {boolean} isLong
* @returns {number}
*/
targetTicks(isLong) {
const stopPrice = this.suggestedStop(isLong)
const riskTicks = isNaN(stopPrice)
? params.stopTicks
: market.toTicks(Math.abs(market.price - stopPrice))
return Math.max(1, Math.round(riskTicks * params.riskReward))
}
}
BosRetest
Quantum also ships with working strategies to read and adapt, including the 80 Percent Rule, Break of Structure, Failed Breakout to POC, LVN Pullback Continuation, Stacked Imbalance, SSL Flip and Structure Break CVD.
Writing them with the AI
Quantum's AI reads this same reference before it writes a strategy. Describe a setup — “backtest a BOS retest on my last 20 sessions”, “test an EMA 9/21 crossover and compare” — and it writes the file, compiles it, runs the backtest and walks you through the log. The file lands in your library like any other, so you can read and change every line.
An outside client — Claude Code, Claude Desktop or any MCP client — can drive Quantum the same way over a local connection. See the AI Control tool reference.
AI Control uses your own AI subscription or a local model. See AI Control.
Pitfalls
Forgetting the last line
The file must end with the class name on its own line.
Running on the timeframe you meant to read
A backtest replays one day at a time and the strategy's own bar series starts empty each morning, so a coarse series never prints enough bars to confirm a swing: on 1-hour bars market.structure finds nearly nothing and the rule takes zero trades while looking merely unprofitable. Write it as a 5-minute strategy reading structure1h instead — those higher series are carried across days and hold the whole history. Under about 50 bars a day, the backtest log now says so.
Reaching for a bare c
market, broker, params and log are the only names available without this. Anything else, c included, is a ReferenceError on the first bar.
market.bar1m() on a coarse run
A backtest folds its timeframes upward and cannot make a minute bar out of 5-minute ones, so this raises rather than returning an empty object. Guard it by the run's own period, not by testing the result.
Reading the future
market.bar(0) is the bar that just closed; there is no access to the forming bar, and higher-timeframe reads are cut by time. Do not work around it, for example by caching values in init().
Treating missing as a number
Empty objects and undefined mean no answer. Guard every level read: a missing gate fails closed in a plain test and OPEN in a negated one.
Keying on a state instead of an event
fast > slow is true on every bar of a trend; the cross is the event. The same goes for market.structure.bosNow versus a structure level.
Updating indicators inside signal()
signal() is skipped while a position is open, so a running value updated there misses every bar held in a trade. Update in onBar() and call super.onBar().
Relying on the trail in a backtest
A backtest cannot amend a resting order, so the stop stays where it was placed. Keep a bar-close exit as the fallback.
Order book rules in a backtest
There is no book in a backtest; test DOM rules on a replay.
Backfilled delta
deltaFromBar true means delta is missing, not zero. On stored bars, delta is an estimate and there is no footprint; order-flow rules need tick tapes.
Small samples and zero slippage
A handful of trades proves nothing, and a result that only works at zero slippage is not a result.
onTick cost
Defining onTick() makes every print a call. Only define it when a rule truly needs per-print timing.
Futures trading involves substantial risk of loss. The strategies on this page document Quantum's scripting API; they are not trading advice or a recommendation to trade any particular way. Backtest results are hypothetical and have inherent limitations. Test any strategy on a replay or simulation account first. See our full disclaimer.