Guide · Strategy tester
The backtest console: test trading strategies with code
Every chart has a Strategy button. It opens a console that replays a strategy over past candles and shows each trade on the chart, the profit, the drawdown, the win rate and an equity curve against buy and hold. Start with a built-in strategy, then write your own in a few lines of JavaScript.
Safe by design. Your script runs only in your browser, in a sandbox with no internet access: it can read the candles it is given and nothing else (not your account, not this site, not other websites). Nothing you write is run on our servers.
Start in one minute
- Open any chart, for example EURUSD, and pick the timeframe you want to test (H1, D1…).
- Press Strategy in the chart's toolbar. The console opens under the chart.
- Choose a built-in strategy, for example Moving average crossover. It runs at once: arrows on the chart mark every entry and exit.
- Change an input (say the slow average from 30 to 50): the test runs again by itself.
- Read Overview for the totals, Trades for each trade, Equity curve for the account over time.
- Press Edit a copy to turn it into your own script, change it, and press Run test (or Ctrl+Enter).
The console, part by part
| Part | What it does |
|---|---|
| Strategy list | Built-in: six ready strategies (moving average crossover, RSI reversal, Bollinger Bands mean reversion, MACD signal cross, Donchian breakout, SuperTrend). My strategies: the ones you saved (sign in). New script… starts an empty one. |
| View code / Edit a copy | View code shows exactly what a built-in does. Edit a copy copies it into the editor as your own script, ready to change. |
| Inputs | Every input(…) in the script becomes a field here (a number or a list). Changing one re-runs the test. Defaults puts them back. |
| Settings | Capital, order size, costs, how many candles and whether short trades are allowed (below). |
| Editor | The JavaScript of the strategy. Tab indents, Ctrl/⌘+Enter runs. An error shows its message and the line number. |
| Reference | A short list of everything a script can use, right inside the console. This page is the long version. |
| Save / Save as… | Keeps the script, its name, inputs and settings in your account (a dot on Save • means unsaved changes). Your strategies are listed on every chart. |
| Results tabs | Overview, Trades (with Download CSV), Equity curve and Log. Trades and plotted lines are also drawn on the chart; click a trade to jump to it. |
Settings
| Setting | Meaning | Good starting value |
|---|---|---|
| Starting capital | The account the test starts with, in the price's currency (for EURUSD: dollars). | 10,000 |
| Order size | % of equity: each trade uses this share of the account at the time (100% = the whole account, no leverage). units: a fixed quantity (for forex, units of the base currency: 100,000 = one standard lot). A script can set its own size per order with { qty }. | 100 % of equity, or a fixed lot |
| Commission % per side | A fee as a percentage of the traded value, charged on the entry and again on the exit. | 0.002–0.01 for forex, 0.05–0.1 for stocks and crypto |
| Slippage / spread (price) | Added against you on every fill, in price units. The candles are bid prices, so put the typical spread here to make buys realistic. | EURUSD 0.00010 (1 pip), gold 0.30 |
| Candles | On the chart: the candles loaded on screen. Signed in, choose Up to 5,000 … 100,000 to load more history of the same timeframe (this site allows up to 20,000). | As many as you can: more candles, more trades, more reliable numbers |
| Allow short trades | Off: short() only closes a long position, it never opens a short one. Handy to test the long side alone without editing the script. | On for forex, off for long-only investing |
Reading the results
| Number | What it tells you |
|---|---|
| Net profit | Final equity minus starting capital, after commission and slippage; also in % of the capital. |
| Buy and hold | What simply buying at the first candle and holding to the last would have made. A strategy that does not beat it, or does so with a much deeper drawdown, adds little. |
| Max drawdown | The largest fall of the equity from a peak, in money and %. The pain you would have had to sit through. Many traders look at net profit ÷ max drawdown. |
| Trades, % won | How many trades closed and the share that made money. A low win rate is fine if the winners are much bigger than the losers. |
| Profit factor | Gross profit ÷ gross loss. Above 1 makes money; 1.3–2 is typical of sound strategies; very high values on few trades are usually luck. |
| Average trade, average win, average loss | Profit per trade on average, and the typical size of winners and losers. Compare the average trade with your costs. |
| Largest win / loss, losing trades in a row | The extremes. One huge winner carrying the whole result is a warning sign. |
| Sharpe ratio | Return per unit of volatility, annualised from the candle-to-candle equity changes. Above 1 is good, above 2 very good (and rare). |
| Time in the market | The share of candles with a position open. Less time for the same profit means less risk. |
| Long / short | Trades, profit and win rate of each side separately: often one side does all the work. |
The Trades tab lists every trade with its direction, entry and exit time and price, size, profit in money and %, how many candles it lasted, why it closed (signal, stop loss, take profit, reversed, still open at the end) and your comment. Download CSV exports it for a spreadsheet. The Equity curve shows the account against buy and hold. The Log shows what your script wrote with log().
How orders are filled
- No looking ahead. An order placed in
onBar(i)is filled at the open of the next candle (i + 1). A strategy can never trade on a price it could not have known. - Stops inside the candle. Stop loss and take profit are checked against each candle's high and low. If both could have been reached in the same candle, the stop counts (the cautious assumption). A gap through a stop fills at the open, as in real markets.
- One position at a time.
long()while short closes the short and opens a long ("reversed");long()while already long only updates the stops you pass. - Costs on every fill. Slippage moves each fill price against you; commission is charged on entry and exit.
- The end. A trade still open after the last candle is closed at the last close and marked "still open at the end".
Writing your own strategy
A strategy is a short JavaScript program in two parts:
- The top runs once: read your inputs with
input(), calculate whole series withta.*orind(), and draw them withplot(). function onBar(i) { … }runs for every candle, oldest first.iis the candle's number (0 = the oldest). Decide here:long(),short(),exit(), or nothing.
// 1. once: inputs and series
const len = input("Average length", 50, { min: 5, max: 400 });
const avg = ta.sma(close, len); // one value per candle
plot(avg, "SMA " + len, "#2F80ED"); // drawn on the price chart
// 2. every candle
function onBar(i) {
if (avg[i] == null) return; // the first len - 1 values are null (warming up)
if (position === 0 && close[i] > avg[i]) long();
if (position > 0 && close[i] < avg[i]) exit("below the average");
}
Things worth knowing from the start:
- Series are arrays, one value per candle, oldest first:
close[i]is this candle's close,close[i - 1]the one before. Calculated series arenulluntil they have enough candles; check before using them. - Inside
onBar(i)you can read candles up toionly.close[i + 1]stops the test with an error: that would be peeking into the future. - Use
constandletas usual; your own variables keep their values between candles (handy for a trailing stop). - Prices are in the chart's own units (for EURUSD 1.0850, for gold 2650.40); time is UTC seconds.
Reference
Candles and the current state
| Name | What it is |
|---|---|
open, high, low, close, volume | Arrays of the candles' values, oldest first (volume is the tick volume). |
time | Each candle's opening time in UTC seconds. new Date(time[i] * 1000).getUTCHours() gives the hour. |
bars, symbol, timeframe | The number of candles, the ticker (e.g. "EURUSD") and the timeframe (e.g. "H1"). |
position | Inside onBar: 1 long, -1 short, 0 flat. |
positionSize, entryPrice, entryBar | The open trade's size in units, fill price and candle number (null when flat). i - entryBar = candles held. |
equity, cash | The account value at this candle's close (with the open trade), and the cash without it. |
Inputs
| Call | Result |
|---|---|
input("Period", 14, { min: 2, max: 100, step: 1 }) | A number field (kept between min and max). Returns the value the customer typed, or 14. |
input("Average", "EMA", { options: ["SMA", "EMA"] }) | A list. Returns the chosen text. |
Up to 30 inputs. Their values are saved with the strategy and travel with it when you publish it in an idea.
Calculations (ta.*)
| Function | Returns |
|---|---|
ta.sma(src, n), ta.ema(src, n), ta.wma(src, n), ta.rma(src, n) | Simple, exponential, weighted and Wilder's moving average of any series. |
ta.rsi(src, 14) | RSI (0–100). |
ta.atr(14), ta.tr() | Average true range (simple average, as on the chart) and the true range of each candle. |
ta.stdev(src, n) | Standard deviation over n candles. |
ta.highest(src, n), ta.lowest(src, n) | Highest / lowest value of the last n candles, including this one. For a breakout compare with [i - 1]. |
ta.change(src, n = 1), ta.roc(src, n = 1) | Difference to n candles ago, and the same in %. |
ta.macd(src, 12, 26, 9) | { macd, signal, hist }. |
ta.bb(src, 20, 2) | Bollinger Bands { upper, middle, lower }. |
ta.stoch(14, 3, 3) | Stochastic { k, d } (period, %D smoothing, slowing). |
ta.supertrend(10, 3) | { line, dir }: dir is 1 in an up trend, -1 in a down trend. |
src is any series: close, high, another result, or your own array. Every function returns a new array; nothing changes the candles.
Signals
| Function | True when… |
|---|---|
ta.crossover(a, b, i) | a crosses above b on candle i (it was at or below on i - 1). b may be a number: ta.crossover(rsi, 30, i). |
ta.crossunder(a, b, i) | a crosses below b. |
ta.cross(a, b, i) | Either. |
ta.rising(s, n, i), ta.falling(s, n, i) | s went up (down) on each of the last n candles. |
Orders
| Call | Effect (at the next candle's open) |
|---|---|
long(options) (also buy) | Opens a long position; closes a short first. |
short(options) (also sell) | Opens a short position; closes a long first. With short trades off it only closes the long. |
exit("reason") | Closes the open position. The reason shows in the trade list. |
stopLoss(price), takeProfit(price) | Moves the stop / target of the open trade at once (null removes it). Use them for trailing stops. |
| Option | Meaning |
|---|---|
{ sl: 1.0820, tp: 1.0950 } | Stop loss and take profit as prices. |
{ slPct: 2, tpPct: 4 } | Stop and target in % from the fill price. |
{ qty: 50000 } | Size in units for this order (instead of the Order size setting). |
{ comment: "breakout" } | A note shown with the trade. |
Output
| Call | Effect |
|---|---|
plot(series, "Name", "#2F80ED") | Draws a line on the price chart (up to 12). |
log("text", value, …) | Writes a line to the Log tab with the candle's time (up to 200 lines). |
All 71 chart indicators in code: ind()
ind(id, inputs) calculates any indicator of the chart exactly as the chart does. It returns its first line as a series; all lines are in .lines and their names in .names. Inputs you leave out take the default shown. method is one of SMA, EMA, SMMA, LWMA; price one of close, open, high, low, median, typical, weighted.
const st = ind("stoch", { k: 14, d: 3, slowing: 3 }); // st = %K, st.lines[1] = %D
const ich = ind("ichimoku"); // ich.names: Tenkan, Kijun, Span A, Span B, Chikou
const kijun = ich.lines[1];
| id | Indicator | Inputs (default) | Lines |
|---|---|---|---|
ma | Moving Average | period: 20, method: SMA, price: close, shift: 0 | MA |
ama | Adaptive Moving Average | period: 9, fast: 2, slow: 30, price: close | AMA |
adx | Average Directional Movement Index | period: 14 | ADX, +DI, −DI |
adxw | ADX Wilder | period: 14 | ADX, +DI, −DI |
bb | Bollinger Bands | period: 20, dev: 2, price: close, shift: 0 | Middle, Upper, Lower |
dema | Double Exponential Moving Average | period: 14, price: close | DEMA |
env | Envelopes | period: 14, method: SMA, dev: 0.1, price: close | Upper, Lower |
frama | Fractal Adaptive Moving Average | period: 14, price: close | FrAMA |
ichimoku | Ichimoku Kinko Hyo | tenkan: 9, kijun: 26, senkou: 52 | Tenkan, Kijun, Span A, Span B, Chikou |
sar | Parabolic SAR | step: 0.02, max: 0.2 | SAR |
stddev | Standard Deviation | period: 20, method: SMA, price: close | StdDev |
tema | Triple Exponential Moving Average | period: 14, price: close | TEMA |
vidya | Variable Index Dynamic Average | cmo: 9, ema: 12, price: close | VIDYA |
atr | Average True Range | period: 14 | ATR |
bears | Bears Power | period: 13 | Bears |
bulls | Bulls Power | period: 13 | Bulls |
cci | Commodity Channel Index | period: 14, price: typical | CCI |
chaikin | Chaikin Oscillator | fast: 3, slow: 10, method: EMA | CHO |
demarker | DeMarker | period: 14 | DeMarker |
force | Force Index | period: 13, method: SMA | Force |
macd | MACD | fast: 12, slow: 26, signal: 9, price: close | MACD, Signal |
momentum | Momentum | period: 14, price: close | Momentum |
osma | Moving Average of Oscillator | fast: 12, slow: 26, signal: 9, price: close | OsMA |
rsi | Relative Strength Index | period: 14, price: close | RSI |
rvi | Relative Vigor Index | period: 10 | RVI, Signal |
stoch | Stochastic Oscillator | k: 5, d: 3, slowing: 3, method: SMA | %K, %D |
trix | Triple Exponential Average | period: 14, price: close | TRIX |
wpr | Williams' Percent Range | period: 14 | %R |
ad | Accumulation/Distribution | none | A/D |
mfi | Money Flow Index | period: 14 | MFI |
obv | On Balance Volume | none | OBV |
volumes | Volumes | none | Volume |
ac | Accelerator Oscillator | none | AC |
alligator | Alligator | jaw: 13, jawShift: 8, teeth: 8, teethShift: 5, lips: 5, lipsShift: 3 | Jaw, Teeth, Lips |
ao | Awesome Oscillator | none | AO |
fractals | Fractals | none | Up, Down |
gator | Gator Oscillator | jaw: 13, teeth: 8, lips: 5 | Upper, Lower |
bwmfi | Market Facilitation Index | none | BW MFI |
zigzag | ZigZag | depth: 12, dev: 0.3 | ZigZag |
vwap | VWAP (volume-weighted average price) | none | VWAP |
supertrend | SuperTrend | period: 10, mult: 3 | Up trend, Down trend |
keltner | Keltner Channels | period: 20, atr: 10, mult: 2 | Middle, Upper, Lower |
donchian | Donchian Channels | period: 20 | Upper, Lower, Middle |
hma | Hull Moving Average | period: 20, price: close | HMA |
pivots | Pivot Points (classic) | none | P, R1, S1, R2, S2, R3, S3 |
stochrsi | Stochastic RSI | rsi: 14, stoch: 14, k: 3, d: 3 | %K, %D |
aroon | Aroon | period: 25 | Aroon up, Aroon down |
cmf | Chaikin Money Flow | period: 20 | CMF |
uo | Ultimate Oscillator | p1: 7, p2: 14, p3: 28 | UO |
chop | Choppiness Index | period: 14 | CHOP |
vwma | Volume-Weighted Moving Average | period: 20 | VWMA |
zlema | Zero-Lag Exponential Moving Average | period: 21, price: close | ZLEMA |
alma | Arnaud Legoux Moving Average | period: 9, offset: 0.85, sigma: 6, price: close | ALMA |
mcginley | McGinley Dynamic | period: 14 | McGinley |
lsma | Least Squares Moving Average | period: 25, price: close | LSMA |
chandelier | Chandelier Exit | period: 22, mult: 3 | Long exit, Short exit |
roc | Rate of Change | period: 12 | ROC |
cmo | Chande Momentum Oscillator | period: 9 | CMO |
tsi | True Strength Index | long: 25, short: 13, signal: 13 | TSI, Signal |
ppo | Percentage Price Oscillator | fast: 12, slow: 26, signal: 9 | Histogram, PPO, Signal |
dpo | Detrended Price Oscillator | period: 20 | DPO |
kst | Know Sure Thing | signal: 9 | KST, Signal |
vortex | Vortex Indicator | period: 14 | VI+, VI− |
eom | Ease of Movement | period: 14 | EOM |
mass | Mass Index | ema: 9, sum: 25 | Mass Index |
coppock | Coppock Curve | wma: 10, long: 14, short: 11 | Coppock |
hv | Historical Volatility | period: 20 | HV % |
fisher | Fisher Transform | period: 9 | Fisher, Trigger |
squeeze | Squeeze Momentum | bb: 20, bbk: 2, kc: 20, kck: 1.5 | Momentum, Squeeze on, Squeeze off |
crsi | Connors RSI | rsi: 3, streak: 2, rank: 100 | CRSI |
pvt | Price Volume Trend | none | PVT |
Examples
Each example runs as it is: press Copy, open the console, choose New script…, paste and press Run test. Results depend on the ticker, timeframe and period; the examples teach the technique, they are not recommendations.
1. Your first strategy: buy above a moving average
The smallest useful script. It is long while the close is above its 50-candle average and flat otherwise. The top of the script runs once; onBar(i) runs for every candle.
const len = input("Average length", 50, { min: 5, max: 400 });
const avg = ta.sma(close, len);
plot(avg, "SMA " + len, "#2F80ED");
function onBar(i) {
if (avg[i] == null) return; // still warming up
if (position === 0 && close[i] > avg[i]) long({ comment: "close above the average" });
if (position > 0 && close[i] < avg[i]) exit("close below the average");
}
2. Moving average crossover, long and short
Always in the market: long() while short reverses the position (and short() while long). Untick Allow short trades in the settings to make it long-only without changing the code.
const fast = ta.ema(close, input("Fast EMA", 12, { min: 2, max: 200 }));
const slow = ta.ema(close, input("Slow EMA", 26, { min: 3, max: 400 }));
plot(fast, "Fast", "#2F80ED");
plot(slow, "Slow", "#E39B2D");
function onBar(i) {
if (ta.crossover(fast, slow, i)) long();
if (ta.crossunder(fast, slow, i)) short();
}
3. Fixed stop loss and take profit in percent
slPct and tpPct are measured from the fill price. The stop and the target are checked inside every candle with its high and low; if both could have been hit in one candle, the stop counts.
const r = ta.rsi(close, 14);
const sl = input("Stop loss %", 1.5, { min: 0.1, max: 20, step: 0.1 });
const tp = input("Take profit %", 3, { min: 0.1, max: 50, step: 0.1 });
function onBar(i) {
if (position !== 0) return; // one trade at a time
if (ta.crossover(r, 30, i)) long({ slPct: sl, tpPct: tp, comment: "RSI back above 30" });
if (ta.crossunder(r, 70, i)) short({ slPct: sl, tpPct: tp, comment: "RSI back below 70" });
}
4. ATR stop and a 2:1 target in prices
sl and tp take prices. Here the stop sits 2 ATR from the close and the target twice as far (risk/reward 1:2). The fill is at the next open, so the real distance can differ slightly.
const a = ta.atr(14);
const hi = ta.highest(high, 20);
const mult = input("Stop (ATR multiple)", 2, { min: 0.5, max: 6, step: 0.5 });
function onBar(i) {
if (i < 1 || a[i] == null || hi[i - 1] == null || position !== 0) return;
if (close[i] > hi[i - 1]) { // a new 20-candle high
const stop = close[i] - mult * a[i];
const target = close[i] + 2 * mult * a[i];
long({ sl: stop, tp: target, comment: "breakout, risk " + (mult * a[i]).toFixed(5) });
}
}
5. A trailing stop that follows the price
stopLoss(price) moves the stop of the open trade. This one trails 3 ATR under the highest close since the entry and never moves down.
const a = ta.atr(14);
const fast = ta.ema(close, 20), slow = ta.ema(close, 50);
let stop = null;
function onBar(i) {
if (a[i] == null || slow[i] == null) return;
if (position === 0) {
stop = null;
if (ta.crossover(fast, slow, i)) long({ comment: "trend up" });
return;
}
const candidate = close[i] - 3 * a[i];
if (stop === null || candidate > stop) { // only ever raise it
stop = candidate;
stopLoss(stop);
}
}
6. Any chart indicator with ind(): ADX trend filter
ind(id, inputs) gives the first line of any of the chart's 71 indicators; the other lines are in .lines (their names in .names). Here: trade the MACD cross only when ADX says the market is trending.
const adx = ind("adx", { period: 14 }); // .lines[0] ADX, [1] +DI, [2] -DI
const plusDI = adx.lines[1], minusDI = adx.lines[2];
const m = ta.macd(close, 12, 26, 9);
const minAdx = input("Minimum ADX", 25, { min: 10, max: 50 });
function onBar(i) {
if (adx[i] == null || adx[i] < minAdx) return;
if (ta.crossover(m.macd, m.signal, i) && plusDI[i] > minusDI[i]) long();
if (ta.crossunder(m.macd, m.signal, i) && minusDI[i] > plusDI[i]) short();
}
7. Mean reversion with Bollinger Bands
Buys a close under the lower band, sells a close over the upper band, and exits at the middle band. A classic for ranging markets; it suffers in strong trends.
const b = ta.bb(close, input("Period", 20, { min: 5, max: 200 }), input("Width", 2, { min: 0.5, max: 4, step: 0.1 }));
plot(b.upper, "Upper", "#8E44AD");
plot(b.lower, "Lower", "#8E44AD");
function onBar(i) {
if (b.middle[i] == null) return;
if (position === 0 && close[i] < b.lower[i]) long({ comment: "below the lower band" });
else if (position === 0 && close[i] > b.upper[i]) short({ comment: "above the upper band" });
else if (position > 0 && close[i] >= b.middle[i]) exit("middle band");
else if (position < 0 && close[i] <= b.middle[i]) exit("middle band");
}
8. Trade only at certain hours (time filter)
time[i] is the candle's open in UTC seconds. This example only opens trades between 07:00 and 16:00 UTC (the London session) and closes anything still open at 20:00. Use it on an intraday timeframe.
const from = input("Start hour (UTC)", 7, { min: 0, max: 23 });
const to = input("End hour (UTC)", 16, { min: 1, max: 24 });
const fast = ta.sma(close, 10), slow = ta.sma(close, 30);
function onBar(i) {
const hour = new Date(time[i] * 1000).getUTCHours();
if (position !== 0 && hour >= 20) { exit("end of the day"); return; }
if (hour < from || hour >= to) return;
if (ta.crossover(fast, slow, i)) long();
if (ta.crossunder(fast, slow, i)) short();
}
9. Your own calculation: a volatility breakout
Series are plain arrays, so you can compute your own. This builds yesterday's range for every candle and buys a break above the open plus half of it. log() writes to the Log tab.
const k = input("Range multiple", 0.5, { min: 0.1, max: 2, step: 0.1 });
const range = high.map((h, i) => (i ? high[i - 1] - low[i - 1] : null)); // the previous candle's range
const trigger = open.map((o, i) => (range[i] == null ? null : o + k * range[i]));
plot(trigger, "Trigger", "#16A085");
function onBar(i) {
if (trigger[i] == null) return;
if (position === 0 && close[i] > trigger[i]) {
long({ slPct: 1, comment: "close above open + " + k + " x range" });
log("signal at", close[i], "trigger", trigger[i]);
} else if (position > 0 && i - entryBar >= 3) {
exit("held 3 candles");
}
}
10. Position size from risk (1% of equity per trade)
{ qty } sets the size in units for one order, overriding the Order size setting. Here the size is chosen so that hitting the stop loses about 1% of the current equity.
const a = ta.atr(14);
const riskPct = input("Risk per trade %", 1, { min: 0.1, max: 5, step: 0.1 });
const fast = ta.ema(close, 20), slow = ta.ema(close, 50);
function onBar(i) {
if (a[i] == null || slow[i] == null || position !== 0) return;
if (ta.crossover(fast, slow, i)) {
const stopDistance = 2 * a[i];
const qty = (equity * riskPct / 100) / stopDistance;
long({ qty: qty, sl: close[i] - stopDistance, tp: close[i] + 3 * stopDistance,
comment: "risk " + riskPct + "%" });
}
}
11. Two conditions in a row: pullback in an uptrend
Combines a trend filter (price above the 200 average), a pullback (RSI under 40 within the last 5 candles) and a trigger (close back above the 20 average).
const trend = ta.sma(close, 200), fast = ta.ema(close, 20), r = ta.rsi(close, 14);
function onBar(i) {
if (trend[i] == null || i < 5) return;
let dipped = false;
for (let k = 0; k < 5; k++) if (r[i - k] != null && r[i - k] < 40) dipped = true;
if (position === 0 && close[i] > trend[i] && dipped && ta.crossover(close, fast, i)) {
long({ slPct: 2, tpPct: 4, comment: "pullback bought" });
}
}
Errors and how to fix them
| Message | Cause and fix |
|---|---|
| the script needs a function onBar(i) { … } | The script has no onBar, or it is misspelt (onbar). Write function onBar(i) { … } at the top level of the script, not inside another function. |
| close[124] is in the future: inside onBar(123) only candles up to 123 are known | You read a later candle. Use close[i] and earlier (close[i - 1]). For a breakout, compare today's close with yesterday's highest high: close[i] > hi[i - 1]. |
| close cannot be changed | The candles are read-only. Make your own array instead: const mid = close.map((c, i) => (c + open[i]) / 2); |
| Cannot read properties of null / NaN in the results | A series is still null at the start (warming up). Begin onBar with if (avg[i] == null) return;, and check i >= 1 before using [i - 1]. |
| unknown indicator "…" | Use an id from the indicator table (lower case), e.g. ind("adx"), not ind("ADX"). |
| No trades: the strategy never placed an order | The conditions never became true together. Add log("rsi", r[i]) to see the values, loosen a condition, or load more candles. |
| The strategy took longer than 15 seconds (an endless loop?) | A loop that never ends, or a heavy loop inside onBar. Calculate series once at the top instead of looping over all candles on every candle. |
| Unexpected token / SyntaxError (line N) | A typing mistake on that line: a missing bracket, quote or comma. |
Testing that you can trust
- Add your costs. A strategy that trades often can look great with zero costs and lose money with a realistic spread. Always fill in slippage (the spread) and commission.
- Use enough trades. Under about 30 trades, the numbers are mostly chance. Load more candles or use a lower timeframe.
- Do not fit the past. Trying many inputs until the result is perfect finds the noise of that period, not a rule that works. Tune on one part of the history, then check the same inputs on another period and on a few other tickers.
- Compare with buy and hold and look at the drawdown, not only the profit.
- Watch the extremes. If one or two trades make all the profit, the strategy depends on luck.
- Practise before risking money. Run the strategy forward on paper trading or with bar replay, and set alerts for its signals.
Back-tests use past bid candles and ignore swaps, financing and spreads that change with the market. Past results do not guarantee future results. This is not investment advice.
Save, share, publish
- Save (signed in) keeps up to 50 strategies with their inputs and settings; they appear in the list on every chart.
- Publish an idea from the chart and attach a strategy: readers can open your chart and run the strategy themselves, with your inputs. See Ideas.
- Share your analysis with a team in a club.
- Download CSV on the Trades tab exports the trades to a spreadsheet.
- For heavier research, pull the same candles with the price API (
GET /bars) into Python, R or Excel.
Limits
| Candles per test | Visitors: the candles on the chart. Signed in: up to 20,000 (the Premium plans can raise this; see Premium). |
| Script | 50,000 characters, 30 inputs, 12 plotted lines, 200 log lines, 5,000 trades listed, 15 seconds of running time. |
| Saved strategies | 50 per account. |
| Positions | One position at a time per test (long, short or flat); one ticker and timeframe per test. |
Questions
Do I need to know programming?
No. The built-in strategies work with no code; you only change their inputs. When you want more, Edit a copy gives you a working script to change one line at a time, and the examples above cover the usual building blocks.
Is this like Pine Script?
The idea is the same (series, inputs, plots, orders), the language is plain JavaScript. Most simple Pine strategies translate line by line: ta.sma(close, 20) stays ta.sma(close, 20), strategy.entry("L", strategy.long) becomes long(), and the body of the script goes into onBar(i) with [i] after each series.
Can the strategy trade for me?
No. The console only tests on past candles. To follow a strategy live, use paper trading and alerts, or call the price API from your own program.
Why is my result different from another platform's?
Different candles (bid vs mid prices, the broker's session times), fills at the next open instead of the close, costs, and the stop-before-target rule all change results. Compare like with like.
Is my code private?
Yes. It runs in your browser; it is stored in your account only when you press Save, and other people see it only if you publish it in an idea.