Build your first strategy

View as MarkdownOpen the editor

One idea taken from a sentence to a backtest you can evaluate honestly: write the strategy as one wrun indicator file in the chart's editor, backtest it, read every part of the Strategy Tester, then tune it like you mean it. It is the strategy sibling of the first indicator primer.

  1. The idea, in one sentence

    "When RSI leaves oversold, buy the dip with a quarter of my equity; take the trade off when momentum recovers; protect it with a stop 4% below."

  2. Write it

    Open the editor from the chart toolbar's Editor button. In the Explorer, press New indicator: the draft opens with a moving-average starter under a //@lang=wrun-ts line, the marker that makes the tab a wrun indicator. Keep that line and replace everything under it with this file:

    // RSI reversion with a protective stop: buy the dip on the bar RSI crosses up through oversold, protect it 4% under the close, leave on recovery.
    strategy({ initialCapital: 10000, qtyType: "percentOfEquity", qtyValue: 25, commissionPercent: 0.05, slippageBps: 2 });
    param("period", 14, { min: 2, max: 200, description: "RSI length in bars" });
    output("rsi", line, lower, { description: "Wilder RSI of close, 0 to 100" });
    
    let rsi = new Rsi(14);
    const dip = new Cross();
    const recovery = new Cross();
    
    function onStart(): void {
      rsi = new Rsi(i32(p_period()));
    }
    
    function onBar(): void {
      const close = bar.close();
      const value = rsi.update(close);
      if (isNaN(value)) return;
      const dipped = dip.update(value, 30.0);
      const recovered = recovery.update(value, 55.0);
      if (dipped == 1) strategy.long("Dip").send();
      if (strategy.positionSize() > 0) strategy.exit("Protect").from("Dip").stop(close * 0.96).send();
      if (recovered == 1) strategy.closeAll();
      out_rsi(value);
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 2 to Aug 18, 2026Real output from OpenMarket's engine

    Reading it top to bottom:

    • strategy({ ... }) is the broker. Starting equity, sizing (25% of equity per entry), commission and slippage live here, so a published strategy carries its own assumptions. It takes the engine's own setting names; every field is optional.
    • Cross.update(a, b) returns 1 on the bar a crosses above b, so dipped == 1 is the bar RSI crosses up through oversold. Returning early from onBar() while RSI warms up keeps those bars off the chart and out of the broker's reach: a bar that returns before the order calls places nothing, and its unwritten rsi is NaN, which is not drawn.
    • strategy.long("Dip").send() queues a market order under the id Dip. It fills at the next bar's open, never earlier.
    • strategy.exit("Protect").from("Dip").stop(close * 0.96).send() arms a protective stop against the named entry, re-placed every bar the position is held, at 4% under that bar's close. strategy.positionSize() reads the position after the bar's fills, so the guard is already flat on the bar the stop filled.
    • strategy.closeAll() is the signal exit for the recovery case. Order calls and the getters both belong in onBar().
  3. Backtest it

    The editor compiles the file in your browser as you type, and each build derives the sheet from it (the two declarations, the close input the file reads through bar.close(), and the strategy section). Once the file builds, the toolbar shows a Strategy badge and the Run button reads Backtest. Press it: the draft goes on the chart's price pane, RSI gets a pane of its own below, and the Strategy Tester docks under the chart. The Tester runs the file in your browser over the candles the chart has loaded, lets the broker fill every order against those candles, and recomputes as you pan, so panning back tests more history. A problem in the file stops the build instead, and prints in the editor's Console with the stage it came from.

  4. Read the result critically

    Four places to look, in order:

    1. Run details. The info icon in the Tester's header opens "Run details": the chart's interval, the depth ("Follows chart (recomputes as you pan)"), the bars the run covered, the declared fill model and the compute time, then a precision line: "Fill precision: exact" when no bar had to be settled by the fill model, "Fill precision: bar resolution" once one was. The costs are the file's own commissionPercent and slippageBps: a strategy that declares none is a gross result, whatever its return says.
    2. The numbers. The Overview's net profit, win rate, profit factor, drawdown, Sharpe and trade count, and the Performance tab's full table, read together, never one alone: a strategy that wins 70% of the time with a payoff ratio of 0.3 loses money. Every stat has an exact formula in the stats reference.
    3. The trades. The Trades tab lists every closed trade with its exit reason under Exit via (signal, stop, limit, trail, closeAll, or Liquidated), then the open trade and the pending orders. A ± fill badge marks a trade the fill model settled where one bar touched both the stop and the target, and Model-settled fills in the Performance tab counts them (ambiguousFillCount): the Tester says so instead of hiding it.
    4. The chart. Every fill is a marker on the bar that filled it. Hover a row in the Trades tab to highlight its trade on the chart, or click it to scroll the chart to the entry, and check that each entry sits where the rule says it should.
  5. Tune it honestly

    Results are deterministic, so every change you see is yours. Change things in this order:

    • Costs first. Set commissionPercent and slippageBps to what you actually pay on your venue. Most retail strategies die here, and it is cheaper to learn that from the Tester than from a book.
    • Sizing. Try qtyType: "fixed" with a small quantity against percentOfEquity. Compounding changes the drawdown's character, not only the end number.
    • The stop. Tighten 0.96 to 0.99 and watch the Exit via mix shift from signal to stop. A stop narrow relative to the bar range asks intrabar questions the interval cannot answer; read fill simulation before tightening further.
    • A setting as a parameter. Link a numeric setting to a param and it becomes a number field in the strategy's settings dialog (the Tester's Strategy settings gear opens it). Change the value and the strategy reruns over the same candles at the new risk, without a recompile.
    // The same reversion with the risk per entry as a setting: the strategy's qtyValue reads the risk_pct param when the run starts.
    const risk = param("risk_pct", 25, { min: 1, max: 100, description: "Percent of equity per entry" });
    strategy({ initialCapital: 10000, qtyType: "percentOfEquity", qtyValue: risk, commissionPercent: 0.05, slippageBps: 2 });
    param("period", 14, { min: 2, max: 200, description: "RSI length in bars" });
    output("rsi", line, lower, { description: "Wilder RSI of close, 0 to 100" });
    
    let rsi = new Rsi(14);
    const dip = new Cross();
    const recovery = new Cross();
    
    function onStart(): void {
      rsi = new Rsi(i32(p_period()));
    }
    
    function onBar(): void {
      const close = bar.close();
      const value = rsi.update(close);
      if (isNaN(value)) return;
      const dipped = dip.update(value, 30.0);
      const recovered = recovery.update(value, 55.0);
      if (dipped == 1) strategy.long("Dip").send();
      if (strategy.positionSize() > 0) strategy.exit("Protect").from("Dip").stop(close * 0.96).send();
      if (recovered == 1) strategy.closeAll();
      out_rsi(value);
    }
    BTCUSDT perpetual on Binance, 1 hour bars, Aug 2 to Aug 18, 2026Real output from OpenMarket's engine

    A fuller version is a starter in the editor's template picker: the Explorer's Templates icon ("Browse starter templates") lists it as Risk-Sized Reversion under Strategies (strategy-risk-reversion), with the stop and a target sized in ATR, each entry sized so a trade stopped out loses risk_pct of the equity (never more than the equity buys), and each trade's rails drawn on price (Risk-sized reversion), commented throughout. In the file above, param(...) returns a handle; passing it as qtyValue derives "qty_value": { "param": "risk_pct" } into the sheet, and the broker reads the param once, when the run starts. A value outside the setting's rule (here, not a positive number) falls back to the default, the engine's own behavior for a bad override.

  6. Keep it

    The editor saves the file to your account as you type, but the strategy on the chart is a draft: it lasts for the session and is not saved with the layout. Publish in the editor turns it into a versioned indicator that you, the people you invite, or everyone can add to a chart, with its costs and sizing inside it (Publishing). A published strategy can also carry alerts from the chart: beside its outputs (rsi here), the alert dialog offers four choices of its own, "Strategy order placed", "Strategy trade opened or closed", "Strategy position" and "Strategy equity", and OpenMarket's alerts engine evaluates them in the cloud (Alerts).

  7. What you have, and what you do not

    You have a deterministic, lookahead-free replay of your rules with disclosed costs and disclosed shortcuts, wherever the file runs. You do not have a promise about the future: no backtest survives contact with a regime change, and a parameter tuned until the curve looks good is a fit to the past. Prefer fewer parameters, realistic costs, and results that stay acceptable when you nudge every setting.

    Next: the full order API, how fills are simulated, and every stat defined.