Examples: Spot Strategies

Three complete spot strategies, the ports of the three kScript (legacy) examples: a two-line trend follower, a reversion with a protective stop, and bracketed…

Three complete spot strategies, the ports of the three kScript (legacy) examples: a two-line trend follower, a reversion with a protective stop, and bracketed exits. Each is one Indicator file, small enough to read in a minute and a good skeleton for your own. Install one and run it:

om wrun install ./ma-cross
om backtest @you/ma-cross --asset BINANCE_FUTURES:BTCUSDT --window 90d

Moving average cross

The smallest useful strategy: long when the fast EMA crosses above the slow one, flat when it crosses back under. One entry rule, one exit rule.

// Moving average cross as a strategy: long when the fast EMA crosses above the slow one, flat when it crosses back under.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_fast, out_slow } from "./gen/outputs";
import { strategy } from "./gen/strategy";
import { Cross, Ema } from "./sdk/ta";

strategy({ initialCapital: 10000, qtyType: "percentOfEquity", qtyValue: 50, commissionPercent: 0.05, slippageBps: 2 });
input("close", ohlcv.close);
output("fast", line, overlay, { description: "9-period EMA of close" });
output("slow", line, overlay, { description: "21-period EMA of close" });

const fastEma = new Ema(9);
const slowEma = new Ema(21);
const cross = new Cross();
let fast: f64 = NaN;
let slow: f64 = NaN;
let crossed: i32 = 0;

export function init(): void {}

export function state(): i32 {
  fast = fastEma.update(in_close());
  slow = slowEma.update(in_close());
  if (isNaN(fast) || isNaN(slow)) return 0;
  crossed = cross.update(fast, slow);
  return 1;
}

export function finalize(): void {
  if (crossed == 1) strategy.long("L").send();
  if (crossed == -1) strategy.closeAll();
  out_fast(fast);
  out_slow(slow);
  emitRow();
}

export function reset(): void {
  fastEma.reset();
  slowEma.reset();
  cross.reset();
  fast = NaN;
  slow = NaN;
  crossed = 0;
}

What to notice: percentOfEquity sizing keeps the position size proportional as equity compounds, and commissionPercent plus slippageBps make the run pay realistic costs on every fill.

RSI reversion with a protective stop

Buys oversold dips and adds a protective stop under every entry, so a dip that keeps dipping gets cut instead of riding to the bottom.

// 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.
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_rsi } from "./gen/outputs";
import { p_period } from "./gen/params";
import { strategy } from "./gen/strategy";
import { Cross, Rsi } from "./sdk/ta";

strategy({ initialCapital: 10000, qtyType: "percentOfEquity", qtyValue: 25, commissionPercent: 0.05, slippageBps: 2 });
param("period", 14, { min: 2, max: 200, description: "RSI length in bars" });
input("close", ohlcv.close);
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();
let value: f64 = NaN;
let close: f64 = NaN;
let dipped: i32 = 0;
let recovered: i32 = 0;

export function init(): void {
  rsi = new Rsi(i32(p_period()));
}

export function state(): i32 {
  close = in_close();
  value = rsi.update(close);
  if (isNaN(value)) return 0;
  dipped = dip.update(value, 30.0);
  recovered = recovery.update(value, 55.0);
  return 1;
}

export function finalize(): void {
  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);
  emitRow();
}

export function reset(): void {
  rsi.reset();
  dip.reset();
  recovery.reset();
  value = NaN;
  close = NaN;
  dipped = 0;
  recovered = 0;
}

What to notice: strategy.exit(...).from("Dip") scopes the stop to the named entry, and the stop follows the close at placement time. The first strategy page builds this file up line by line.

Trend entries with bracket exits

A crossover entry bracketed by both a stop and a take-profit limit. Whichever side the market touches first closes the trade and cancels the other.

// Trend entries with bracket exits: a stop and a take-profit limit on one exit id, whichever the market touches first closes the trade and cancels the other.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_fast, out_slow } from "./gen/outputs";
import { strategy } from "./gen/strategy";
import { Cross, Sma } from "./sdk/ta";

strategy({ initialCapital: 10000, qtyType: "fixed", qtyValue: 1, pyramiding: 1, fillModel: "pathHeuristic" });
input("close", ohlcv.close);
output("fast", line, overlay, { description: "5-period SMA of close" });
output("slow", line, overlay, { description: "20-period SMA of close" });

const fastSma = new Sma(5);
const slowSma = new Sma(20);
const cross = new Cross();
let fast: f64 = NaN;
let slow: f64 = NaN;
let close: f64 = NaN;
let crossed: i32 = 0;

export function init(): void {}

export function state(): i32 {
  close = in_close();
  fast = fastSma.update(close);
  slow = slowSma.update(close);
  if (isNaN(fast) || isNaN(slow)) return 0;
  crossed = cross.update(fast, slow);
  return 1;
}

export function finalize(): void {
  if (crossed == 1) strategy.long("Trend").send();
  if (strategy.positionSize() > 0) strategy.exit("Protect").from("Trend").stop(close * 0.97).limit(close * 1.05).send();
  out_fast(fast);
  out_slow(slow);
  emitRow();
}

export function reset(): void {
  fastSma.reset();
  slowSma.reset();
  cross.reset();
  fast = NaN;
  slow = NaN;
  close = NaN;
  crossed = 0;
}

What to notice: the stop and limit placed through one strategy.exit call form a one-cancels-all pair, and fillModel: "pathHeuristic" decides a bar that touches both. Fill simulation explains exactly how.