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 (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 (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.
// 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);
}