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.

Open the console on EURUSD Jump to the examples

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

  1. Open any chart, for example EURUSD, and pick the timeframe you want to test (H1, D1…).
  2. Press Strategy in the chart's toolbar. The console opens under the chart.
  3. Choose a built-in strategy, for example Moving average crossover. It runs at once: arrows on the chart mark every entry and exit.
  4. Change an input (say the slow average from 30 to 50): the test runs again by itself.
  5. Read Overview for the totals, Trades for each trade, Equity curve for the account over time.
  6. 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

PartWhat it does
Strategy listBuilt-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 copyView code shows exactly what a built-in does. Edit a copy copies it into the editor as your own script, ready to change.
InputsEvery input(…) in the script becomes a field here (a number or a list). Changing one re-runs the test. Defaults puts them back.
SettingsCapital, order size, costs, how many candles and whether short trades are allowed (below).
EditorThe JavaScript of the strategy. Tab indents, Ctrl/⌘+Enter runs. An error shows its message and the line number.
ReferenceA 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 tabsOverview, 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

SettingMeaningGood starting value
Starting capitalThe 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 sideA 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
CandlesOn 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 tradesOff: 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

NumberWhat it tells you
Net profitFinal equity minus starting capital, after commission and slippage; also in % of the capital.
Buy and holdWhat 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 drawdownThe 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, % wonHow 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 factorGross 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 lossProfit 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 rowThe extremes. One huge winner carrying the whole result is a warning sign.
Sharpe ratioReturn 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 marketThe share of candles with a position open. Less time for the same profit means less risk.
Long / shortTrades, 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".
onBar(i): long() filled at this open candle i + 1

Writing your own strategy

A strategy is a short JavaScript program in two parts:

  1. The top runs once: read your inputs with input(), calculate whole series with ta.* or ind(), and draw them with plot().
  2. function onBar(i) { … } runs for every candle, oldest first. i is 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 are null until they have enough candles; check before using them.
  • Inside onBar(i) you can read candles up to i only. close[i + 1] stops the test with an error: that would be peeking into the future.
  • Use const and let as 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

NameWhat it is
open, high, low, close, volumeArrays of the candles' values, oldest first (volume is the tick volume).
timeEach candle's opening time in UTC seconds. new Date(time[i] * 1000).getUTCHours() gives the hour.
bars, symbol, timeframeThe number of candles, the ticker (e.g. "EURUSD") and the timeframe (e.g. "H1").
positionInside onBar: 1 long, -1 short, 0 flat.
positionSize, entryPrice, entryBarThe open trade's size in units, fill price and candle number (null when flat). i - entryBar = candles held.
equity, cashThe account value at this candle's close (with the open trade), and the cash without it.

Inputs

CallResult
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.*)

FunctionReturns
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

FunctionTrue 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

CallEffect (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.
OptionMeaning
{ 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

CallEffect
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];
idIndicatorInputs (default)Lines
maMoving Averageperiod: 20, method: SMA, price: close, shift: 0MA
amaAdaptive Moving Averageperiod: 9, fast: 2, slow: 30, price: closeAMA
adxAverage Directional Movement Indexperiod: 14ADX, +DI, −DI
adxwADX Wilderperiod: 14ADX, +DI, −DI
bbBollinger Bandsperiod: 20, dev: 2, price: close, shift: 0Middle, Upper, Lower
demaDouble Exponential Moving Averageperiod: 14, price: closeDEMA
envEnvelopesperiod: 14, method: SMA, dev: 0.1, price: closeUpper, Lower
framaFractal Adaptive Moving Averageperiod: 14, price: closeFrAMA
ichimokuIchimoku Kinko Hyotenkan: 9, kijun: 26, senkou: 52Tenkan, Kijun, Span A, Span B, Chikou
sarParabolic SARstep: 0.02, max: 0.2SAR
stddevStandard Deviationperiod: 20, method: SMA, price: closeStdDev
temaTriple Exponential Moving Averageperiod: 14, price: closeTEMA
vidyaVariable Index Dynamic Averagecmo: 9, ema: 12, price: closeVIDYA
atrAverage True Rangeperiod: 14ATR
bearsBears Powerperiod: 13Bears
bullsBulls Powerperiod: 13Bulls
cciCommodity Channel Indexperiod: 14, price: typicalCCI
chaikinChaikin Oscillatorfast: 3, slow: 10, method: EMACHO
demarkerDeMarkerperiod: 14DeMarker
forceForce Indexperiod: 13, method: SMAForce
macdMACDfast: 12, slow: 26, signal: 9, price: closeMACD, Signal
momentumMomentumperiod: 14, price: closeMomentum
osmaMoving Average of Oscillatorfast: 12, slow: 26, signal: 9, price: closeOsMA
rsiRelative Strength Indexperiod: 14, price: closeRSI
rviRelative Vigor Indexperiod: 10RVI, Signal
stochStochastic Oscillatork: 5, d: 3, slowing: 3, method: SMA%K, %D
trixTriple Exponential Averageperiod: 14, price: closeTRIX
wprWilliams' Percent Rangeperiod: 14%R
adAccumulation/DistributionnoneA/D
mfiMoney Flow Indexperiod: 14MFI
obvOn Balance VolumenoneOBV
volumesVolumesnoneVolume
acAccelerator OscillatornoneAC
alligatorAlligatorjaw: 13, jawShift: 8, teeth: 8, teethShift: 5, lips: 5, lipsShift: 3Jaw, Teeth, Lips
aoAwesome OscillatornoneAO
fractalsFractalsnoneUp, Down
gatorGator Oscillatorjaw: 13, teeth: 8, lips: 5Upper, Lower
bwmfiMarket Facilitation IndexnoneBW MFI
zigzagZigZagdepth: 12, dev: 0.3ZigZag
vwapVWAP (volume-weighted average price)noneVWAP
supertrendSuperTrendperiod: 10, mult: 3Up trend, Down trend
keltnerKeltner Channelsperiod: 20, atr: 10, mult: 2Middle, Upper, Lower
donchianDonchian Channelsperiod: 20Upper, Lower, Middle
hmaHull Moving Averageperiod: 20, price: closeHMA
pivotsPivot Points (classic)noneP, R1, S1, R2, S2, R3, S3
stochrsiStochastic RSIrsi: 14, stoch: 14, k: 3, d: 3%K, %D
aroonAroonperiod: 25Aroon up, Aroon down
cmfChaikin Money Flowperiod: 20CMF
uoUltimate Oscillatorp1: 7, p2: 14, p3: 28UO
chopChoppiness Indexperiod: 14CHOP
vwmaVolume-Weighted Moving Averageperiod: 20VWMA
zlemaZero-Lag Exponential Moving Averageperiod: 21, price: closeZLEMA
almaArnaud Legoux Moving Averageperiod: 9, offset: 0.85, sigma: 6, price: closeALMA
mcginleyMcGinley Dynamicperiod: 14McGinley
lsmaLeast Squares Moving Averageperiod: 25, price: closeLSMA
chandelierChandelier Exitperiod: 22, mult: 3Long exit, Short exit
rocRate of Changeperiod: 12ROC
cmoChande Momentum Oscillatorperiod: 9CMO
tsiTrue Strength Indexlong: 25, short: 13, signal: 13TSI, Signal
ppoPercentage Price Oscillatorfast: 12, slow: 26, signal: 9Histogram, PPO, Signal
dpoDetrended Price Oscillatorperiod: 20DPO
kstKnow Sure Thingsignal: 9KST, Signal
vortexVortex Indicatorperiod: 14VI+, VI−
eomEase of Movementperiod: 14EOM
massMass Indexema: 9, sum: 25Mass Index
coppockCoppock Curvewma: 10, long: 14, short: 11Coppock
hvHistorical Volatilityperiod: 20HV %
fisherFisher Transformperiod: 9Fisher, Trigger
squeezeSqueeze Momentumbb: 20, bbk: 2, kc: 20, kck: 1.5Momentum, Squeeze on, Squeeze off
crsiConnors RSIrsi: 3, streak: 2, rank: 100CRSI
pvtPrice Volume TrendnonePVT

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

MessageCause 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 knownYou 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 changedThe 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 resultsA 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 orderThe 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 testVisitors: the candles on the chart. Signed in: up to 20,000 (the Premium plans can raise this; see Premium).
Script50,000 characters, 30 inputs, 12 plotted lines, 200 log lines, 5,000 trades listed, 15 seconds of running time.
Saved strategies50 per account.
PositionsOne 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.