Skip to main content

Examples

Three complete scripts. Each one is compiled and run by the SDK's test suite, so the code on this page always works with the current SDK.

HTF trend pullback​

A strategy that buys RSI pullbacks on the chart timeframe while a higher-timeframe EMA points up. It sizes each trade by risk and trails the stop once the trade is 1R in profit.

Uses: multi-timeframe, input groups and tooltips, risk-based sizing and a trailing stop, account read-back, alert placeholders.

htf_trend_pullback.ts
// HTF trend pullback (strategy).
//
// Trades pullbacks on the chart timeframe in the direction of a higher-timeframe
// EMA. Shows: request.timeframe + param.timeframe, an indicator running on the
// higher timeframe, risk-based sizing, a trailing stop, account read-back and
// alert placeholders.
import { src, ta, param, config, request, strategy, Side, plot, plotPane, declarePlot, hline, PlotStyle, Stroke, color, alert, declareAlert, isNewBar, math } from "@coinray/strategy";
const tf = param.timeframe("Trend timeframe", "240");
param.meta("Trend timeframe", "Trend", "Must be a multiple of the chart timeframe, up to 1 day");
const trendLen = param.int("Trend EMA", 50, 5, 400);
param.meta("Trend EMA", "Trend");
const rsiLen = param.int("RSI length", 14, 2, 100);
const rsiBuy = param.float("RSI pullback level", 35.0, 5.0, 50.0);
const riskPct = param.float("Risk per trade %", 1.0, 0.1, 10.0);
param.meta("Risk per trade %", "Risk", "Position size so that the stop costs this share of equity");
const atrMult = param.float("Stop ATR multiple", 2.0, 0.5, 10.0);
param.meta("Stop ATR multiple", "Risk");
const trailPct = param.float("Trailing stop %", 1.5, 0.1, 20.0);
param.meta("Trailing stop %", "Risk");
// Top level only. 60 higher-timeframe bars is enough to warm the default EMA.
const htf = request.timeframe(tf, trendLen + 10);
config.warmup(50);
declarePlot("trend", color.orange, 2);
declarePlot("rsi", color.purple, 1, PlotStyle.Line, "rsi");
hline(rsiBuy, color.green, Stroke.Dashed, "rsi");
declareAlert("pullback_long");
export function onBar() {
// Runs on the higher timeframe because its input is an HTF series.
const trend = ta.ema(htf.close, trendLen);
const rsi = ta.rsi(src.close, rsiLen);
const atr = ta.atr(14); // no source argument: always the chart timeframe
plot("trend", trend[1]); // [1] = last CLOSED higher-timeframe bar, never repaints
plotPane("rsi", rsi, "rsi");
if (!isNewBar()) return;
const close = src.close[0];
const uptrend = htf.close[1] > trend[1] && trend[1] > trend[2];
const pullback = rsi[1] < rsiBuy && rsi[0] >= rsiBuy; // RSI crosses back up
if (!uptrend || !pullback || strategy.positionSize() != 0) return;
const stop = math.roundToMintick(close - atrMult * atr[0]);
// Start trailing only once the trade is 1R in profit.
const activation = math.roundToMintick(close + (close - stop));
strategy.order("pullback", Side.Long).marketEntry().stopLoss(stop).trailingStop(trailPct, activation).qtyRiskBased(riskPct).submit();
plot("stop", stop);
alert("pullback_long", '{{ticker}} {{interval}}: long at {{close}}, stop {{plot("stop")}}, trend {{plot("trend")}}');
}

CVD divergence​

An indicator that flags new highs and lows which order flow does not confirm, using the correlation between price and cumulative volume delta.

Uses: order flow, ta.correlation, declared plot styles and levels, labels with callout styles.

cvd_divergence.ts
// CVD divergence (indicator).
//
// Compares price with cumulative volume delta (aggressive buys minus sells).
// When price and CVD stop moving together, the move is not backed by order
// flow. Shows: orderflow(), ta.correlation, declared plot styles, hline, the
// label builder with Pine callout styles, and nz() for markets without data.
//
// Order flow is `na` on exchanges without coverage and in the browser preview;
// the script then simply plots nothing.
import { src, ta, param, config, orderflow, plotPane, declarePlot, hline, PlotStyle, Stroke, color, draw, LabelStyle, Size, YLoc, alert, declareAlert, isNewBar, barTime, na } from "@coinray/strategy";
const len = param.int("Correlation length", 30, 5, 200);
const level = param.float("Divergence below", -0.3, -1.0, 0.5);
param.meta("Divergence below", "", "Price/CVD correlation under this value counts as a divergence");
const lookback = param.int("Swing lookback", 20, 5, 200);
const flow = orderflow(); // top level only
config.warmup(60);
declarePlot("delta", color.withTransp(color.blue, 40), 1, PlotStyle.Columns, "flow");
declarePlot("cvd", color.teal, 2, PlotStyle.Line, "cvd");
declarePlot("corr", color.orange, 1, PlotStyle.Line, "corr");
hline(0.0, color.gray, Stroke.Dotted, "corr");
hline(level, color.red, Stroke.Dashed, "corr");
declareAlert("cvd_bearish_divergence");
declareAlert("cvd_bullish_divergence");
export function onBar() {
const cvd = flow.cvd; // read delta / cvd inside onBar
const corr = ta.correlation(src.close, cvd, len);
plotPane("delta", flow.delta, "flow");
plotPane("cvd", cvd, "cvd");
plotPane("corr", corr, "corr");
// [1]: the newest live bar's order flow is provisional.
if (!isNewBar() || na(corr[1]) || corr[1] >= level) return;
const newHigh = src.high[1] >= ta.highest(src.high, lookback)[1];
const newLow = src.low[1] <= ta.lowest(src.low, lookback)[1];
const t = barTime();
if (newHigh && cvd[1] < ta.highest(cvd, lookback)[1]) {
draw.labelEx(t, src.high[0], "CVD div").textColor(color.white).bg(color.red).size(Size.Small).style(LabelStyle.LabelDown).yloc(YLoc.AboveBar).draw("div_" + t.toString());
alert("cvd_bearish_divergence", "{{ticker}} {{interval}}: new high at {{high}} without CVD confirmation");
} else if (newLow && cvd[1] > ta.lowest(cvd, lookback)[1]) {
draw.labelEx(t, src.low[0], "CVD div").textColor(color.white).bg(color.green).size(Size.Small).style(LabelStyle.LabelUp).yloc(YLoc.BelowBar).draw("div_" + t.toString());
alert("cvd_bullish_divergence", "{{ticker}} {{interval}}: new low at {{low}} without CVD confirmation");
}
}

Weekly VWAP band reversion​

A strategy that fades closes back inside the weekly VWAP bands, targets the VWAP and the opposite band, and skips longs while funding is expensive.

Uses: ta.vwapBands, funding rate with a filter that is safe on spot, a plot fill, boxes and lines with extend and text, barstate and syminfo.

vwap_band_reversion.ts
// Weekly VWAP band reversion (strategy).
//
// Fades stretches beyond the weekly anchored VWAP bands back to the VWAP, and
// skips longs while funding is expensive. Shows: ta.vwapBands with an anchor,
// derivatives() with a na-safe filter, the box and line builders with extend
// and text, plot fills, barstate and syminfo.
import { src, ta, param, config, derivatives, strategy, Side, plot, declarePlot, color, draw, Extend, Size, Align, Stroke, Anchor, isNewBar, barTime, barstate, syminfo, nz, log } from "@coinray/strategy";
const mult = param.float("Band stdev", 2.0, 0.5, 5.0);
const maxFunding = param.float("Max funding % (longs)", 0.03, 0.0, 1.0);
param.meta("Max funding % (longs)", "Filters", "Per funding interval. Ignored on spot and on exchanges without funding data");
const slPct = param.float("Stop beyond band %", 1.0, 0.1, 10.0);
const deriv = derivatives(); // top level only
config.warmup(200);
const band = color.withTransp(color.blue, 85);
declarePlot("vwap", color.blue, 2);
declarePlot("upper", color.withTransp(color.blue, 40), 1, 0, "", "lower", band); // fill upper..lower
declarePlot("lower", color.withTransp(color.blue, 40));
export function onBar() {
const vb = ta.vwapBands(src.hlc3, Anchor.Week, mult);
plot("vwap", vb.vwap);
plot("upper", vb.upper);
plot("lower", vb.lower);
if (barstate.isFirst()) log.info("running on " + syminfo.ticker() + " (" + syminfo.type() + ")");
if (!isNewBar()) return;
const close = src.close[0];
const t = barTime();
// nz(): funding is na on spot, so the filter passes there instead of blocking.
const fundingPct = nz(deriv.fundingRate[0]) * 100.0;
const flat = strategy.positionSize() == 0;
// Re-entry into the bands after a close outside them.
const longSetup = src.close[1] < vb.lower[1] && close > vb.lower[0];
const shortSetup = src.close[1] > vb.upper[1] && close < vb.upper[0];
if (flat && longSetup && fundingPct <= maxFunding) {
const stop = src.low[1] * (1.0 - slPct / 100.0);
strategy.order("fade_long", Side.Long).marketEntry().takeProfit(vb.vwap[0], 0.5).takeProfit(vb.upper[0], 0.5).stopLoss(stop).qtyPercentEquity(25).submit();
zone("long", t, stop, vb.vwap[0], color.green);
} else if (flat && shortSetup) {
const stop = src.high[1] * (1.0 + slPct / 100.0);
strategy.order("fade_short", Side.Short).marketEntry().takeProfit(vb.vwap[0], 0.5).takeProfit(vb.lower[0], 0.5).stopLoss(stop).qtyPercentEquity(25).submit();
zone("short", t, stop, vb.vwap[0], color.red);
}
}
// One trade zone per direction: re-drawing the same id moves it.
function zone(side, t, stop, target, c) {
draw.boxEx(t, stop, t + 3600, target).fill(color.withTransp(c, 88)).border(c, 1, Stroke.Dotted).extend(Extend.Right).text(side + " to VWAP", c, Size.Small, Align.Left, Align.Top).draw("zone_" + side);
draw.lineEx(t, stop, t + 3600, stop).color(c).style(Stroke.Dashed).extend(Extend.Right).label("stop", 1.0, c, Size.Tiny).draw("stop_" + side);
}