📘 INSTRUCTION
🔹 SYNTAX RULES
Code is valid JavaScript executed every tick inside a with(env){...} sandbox. All built-in names below are injected automatically — no imports needed.
• Statements should end with ;
• if blocks require () and {}
• Variables: just assign — fast = ema(close, 9);
• Comments: // line or /* block */
• Standard JS: Math.abs, Math.max, Math.min, Math.floor, Math.sqrt etc. all work
📊 BUILT-IN VARIABLES
| Name | Type | Description |
|---|---|---|
open | number | Current candle open price |
high | number | Current candle high (updates each tick) |
low | number | Current candle low (updates each tick) |
close | number | Latest tick price — use in all calculations |
volume | number | Relative volume this candle |
bar_index | number | Completed bar count from 0 — use for frequency control |
bar_changed | boolean | true only when a new candle just opened. In CRYPTO mode this is true once per candle interval (1m/5m…). In SIM mode always true. Use to guard strategy.add so it fires once per candle, not every 200ms tick. |
// bar_changed: guard strategy.add to fire once per candle (essential in CRYPTO mode)
if (bar_changed) {
strategy.add('long', { ratio: 0.1, sl: slow * 0.97 });
}
// bar_index: run logic only every 5 bars
if (bar_index % 5 === 0) {
// rebalance or re-evaluate less frequently
}
🧮 INDICATOR FUNCTIONS
All are stateless — call any time, no this.xxx setup needed.
| Function | Returns | Description |
|---|---|---|
sma(src, len) | number | Simple moving average of close (len bars) |
ema(src, len) | number | Exponential moving average of close (len bars) |
rsi(len=14) | 0–100 | Wilder RSI — 70+ overbought, 30− oversold |
atr(len=14) | number | Average True Range — volatility unit for SL sizing |
highest(len=20) | number | Highest high of last len completed bars |
lowest(len=20) | number | Lowest low of last len completed bars |
bb(len=20, mult=2) | object | Bollinger Bands → { upper, mid, lower } |
stoch(kLen=14, dLen=3) | object | Stochastic Oscillator → { k, d } both 0–100 |
crossover(a, b) | boolean | true only on the one tick when a crosses above b |
crossunder(a, b) | boolean | true only on the one tick when a crosses below b |
Note sma/ema always use close price. The src argument is accepted for compatibility but ignored.
💡 INDICATOR USAGE PATTERNS
ATR — volatility-scaled stop loss
a = atr(14);
strategy.entry('long', {
ratio: 1.0,
sl: close - 2 * a, // 2 ATR below entry
tp: close + 4 * a // 4 ATR above (2:1 R/R)
});
Bollinger Bands — mean-reversion + squeeze
bands = bb(20, 2);
// Long when price touches lower band
if (close < bands.lower) {
strategy.entry('long', { ratio: 0.5,
sl: bands.lower * 0.98, tp: bands.mid });
}
// Short when price touches upper band
if (close > bands.upper) {
strategy.entry('short', { ratio: 0.5,
sl: bands.upper * 1.02, tp: bands.mid });
}
Stochastic — overbought / oversold filter
s = stoch(14, 3);
fast = ema(close, 9);
slow = ema(close, 21);
// Only enter long if stoch is not overbought
if (crossover(fast, slow) && s.k < 80) {
strategy.entry('long', { ratio: 1.0,
sl: slow * 0.98, tp: close * 1.08 });
}
if (crossunder(fast, slow) && s.k > 20) {
strategy.entry('short', { ratio: 1.0,
sl: slow * 1.02, tp: close * 0.92 });
}
RSI + EMA trend filter
r = rsi(14);
trend = ema(close, 50);
// Only buy dips in an uptrend
if (close > trend && r < 35) {
strategy.entry('long', { ratio: 0.6,
sl: close - 2*atr(14), tp: close * 1.10 });
}
if (close < trend && r > 65) {
strategy.entry('short', { ratio: 0.6,
sl: close + 2*atr(14), tp: close * 0.90 });
}
🎯 STRATEGY COMMANDS
| Command | Behaviour |
|---|---|
strategy.entry('long'/'short', opts?) | Idempotent — closes opposite side, opens this side. No-op if already on same side. |
strategy.add('long'/'short', opts?) | Always opens a new order on that side. Use for pyramiding / scaling in. |
strategy.close('long'/'short') | Close all orders on that side. |
strategy.closeAll() | Close every open order regardless of direction. |
opts — all fields optional, any combination:
| Field | Type | Notes |
|---|---|---|
ratio | 0–1 | Fraction of free cash. Default 1.0. Overridden by qty. |
qty | number | Fixed share count. Takes priority over ratio. |
sl | price | Stop-loss. Long: must be < entry. Short: must be > entry. |
tp | price | Take-profit. Long: must be > entry. Short: must be < entry. |
strategy.entry('long', { ratio: 0.5, sl: close-2*atr(14), tp: close*1.10 });
strategy.entry('short', { qty: 20, sl: close*1.03 });
// Pyramiding: add 20% when price moves in our favour
if (close > this.lastAdd * 1.03) {
strategy.add('long', { ratio: 0.2, sl: ema(close,21)*0.98 });
this.lastAdd = close;
}
📡 POSITION STATE
strategy.position returns a snapshot of current holdings:
| Field | Type | Value |
|---|---|---|
.side | string | 'long' / 'short' / 'none' |
.qty | number | Total shares on that side |
.avgPrice | number | Volume-weighted average entry price |
.unrealized | number | Floating P&L in $ at current price |
pos = strategy.position;
// Gate: only add if already long and profitable
if (pos.side === 'long' && pos.unrealized > 0) {
strategy.add('long', { ratio: 0.2 });
}
// Rolling: close and reopen to compound gains
if (pos.side === 'long' && close > pos.avgPrice * 1.05) {
strategy.close('long');
strategy.entry('long', { ratio: 1.0,
sl: ema(close,21)*0.975, tp: close*1.10 });
}
// Emergency exit: close all on large drawdown
if (pos.unrealized < -200) { strategy.closeAll(); }
Note position reflects state before any commands on the current tick. After strategy.close(), the next tick will show side: 'none'.
💾 PERSISTENT VARIABLES
Use this.myVar to store custom state between ticks. Always guard with === undefined on first tick.
// Track last add price for pyramiding frequency control
if (this.lastAdd === undefined) this.lastAdd = close;
if (close > this.lastAdd * 1.03) {
strategy.add('long', { ratio: 0.2 });
this.lastAdd = close;
}
// Previous value comparison (when crossover() isn't enough)
if (this.prevRsi === undefined) this.prevRsi = rsi(14);
r = rsi(14);
if (this.prevRsi > 50 && r < 50) { /* RSI crossed below 50 */ }
this.prevRsi = r;
When to use Custom rolling sums, counters, previous arbitrary values, cooldown timers. For standard crossovers/indicators, use the built-in functions instead.
📚 EX 1 – TREND PULLBACK
// regime filter + dip entry + chandelier trail (core)
trend = ema(close, 50); // regime line
up = close > trend && ema(close,9) > trend;
if (up && rsi(7) < 35 && pos.side !== 'long') {
strategy.entry('long', { ratio: 0.6, sl: close - 2.5*atr(14) });
this.hh = close; // trail anchor
}
// manual chandelier: sl is FIXED at entry — trail by closing
if (pos.side === 'long') {
this.hh = Math.max(this.hh || close, close);
if (close < this.hh - 3*atr(14)) strategy.close('long');
}
三层结构:EMA(50) 趋势过滤(只顺势开仓)→ RSI(7) 回调择时(趋势内逢低进场)→ 吊灯式移动止盈(从最高点回吐 3·ATR 离场)。SL 在下单后固定不动,所以移动止损必须自己用 this.* + close() 实现——这是本例的核心技巧。
📚 EX 2 – TURTLE (DONCHIAN)
// 20/10 channel + ATR unit pyramiding (core)
if (pos.side === 'none' && high > highest(20)) { // touch trigger
strategy.entry('long', { ratio: 0.25, sl: close - 2*a });
this.units = 1; this.lastAdd = close;
}
// add a unit per 0.5·ATR move, ratchet the shared stop
if (bar_changed && this.units > 0 && this.units < 4 &&
close >= this.lastAdd + 0.5*a) {
this.stop = close - 2*a;
strategy.add('long', { ratio: 0.25, sl: this.stop });
this.units++; this.lastAdd = close;
}
if (pos.side === 'long' && low < lowest(10)) strategy.closeAll();
经典海龟系统:盘中触及 20 棒通道极值即入场(用 high/low 与通道做同类比较——close 对 high 通道几乎永远够不着),每向有利方向走 0.5·ATR 加一个 unit(每个 25% 仓,封顶 4 个),每次加仓把共享止损棘轮上移到新价 −2·ATR,触及 10 棒反向通道离场。this.units / lastAdd / stop 三个状态变量构成完整的单元化仓位管理。
📚 EX 3 – SQUEEZE BREAKOUT
// bandwidth squeeze → directional break (core)
b = bb(20, 2);
bw = (b.upper - b.lower) / b.mid; // normalized bandwidth
this.bws.push(bw); // rolling 120-bar window
if (this.bws.length > 120) this.bws.shift();
lo = Math.min.apply(null, this.bws);
hi = Math.max.apply(null, this.bws);
if (bw <= lo + 0.2*(hi - lo)) this.armed = true; // squeeze!
if (this.armed && close > b.upper) {
strategy.entry('long', { ratio: 0.7, sl: b.mid,
tp: close + 2.5*(close - b.mid) });
this.armed = false;
}
波动率压缩检测:布林带宽落到自身 120 棒区间的最低 20% 分位视为挤压,挂起触发器,随后向哪边破带就做哪边;带宽放大却没破则解除武装。核心技巧:用 this.bws 数组维护滚动统计窗口(内置指标做不到的自定义统计)。
📚 EX 4 – CUSTOM MACD
// hand-rolled MACD: EMA of a DERIVED series (core)
macd = ema(close, 12) - ema(close, 26);
k = 2 / (9 + 1); // signal = EMA(9) of macd
this.signal = this.signal === undefined
? macd : this.signal + k*(macd - this.signal);
hist = macd - this.signal;
// manual zero-cross (avoids conditional crossover() calls)
prevH = this.prevHist === undefined ? hist : this.prevHist;
this.prevHist = hist;
if (prevH <= 0 && hist > 0 && macd > 0)
strategy.entry('long', { ratio: 0.8,
sl: close - 2*atr(14), tp: close + 4*atr(14) });
没有内置 macd()——自己造:信号线是「macd 这条派生序列的 EMA」,内置函数只能算收盘价的 EMA,所以用 this.signal 跨 tick 增量递推。这个模式可以推广到任何自定义指标。出场用「柱体连续两棒衰减」的动能枯竭判断。
📚 EX 5 – Z-SCORE REVERT
// statistical mean reversion, scaled entries (core)
b = bb(20, 1); // mult=1 → upper-mid = 1σ
z = (close - b.mid) / (b.upper - b.mid);
want = z <= -3.4 ? 3 : z <= -2.7 ? 2 : z <= -2 ? 1 : 0;
if (bar_changed && want > this.legs && this.legs < 3) {
if (this.legs === 0) strategy.entry('long', { ratio: 0.25 });
else strategy.add('long', { ratio: 0.25 });
this.legs++;
}
if (pos.side === 'long' && z >= 0) strategy.close('long'); // mean touch
if (pos.side === 'long' && z < -4.5) strategy.close('long'); // regime break
把价格偏离标准化成 z 分数(巧用 bb(20,1):mult=1 时上轨−中轨恰为 1σ),在 −2σ/−2.7σ/−3.4σ 三档分批接刀,回到均值止盈,−4.5σ 认输(统计失效)。刻意只做多:在有正漂移的市场里做空均值回归是负期望。
📚 EX 6 – VOL-TARGETED TREND
// volatility targeting: size by risk budget (core)
atrPct = atr(14) / close; // realized vol proxy
TARGET = 0.012; // per-trade risk budget
size = Math.max(0.15, Math.min(0.9, TARGET / atrPct));
upX = crossover(ema(close,20), ema(close,60)); // unconditional!
downX = crossunder(ema(close,20), ema(close,60));
if (upX) {
strategy.entry('long', { ratio: size, sl: close - 2.2*atr(14) });
this.peak = close;
}
// ratchet trail: 2·ATR giveback from the running peak
if (pos.side === 'long' && close < (this.peak = Math.max(this.peak||close, close)) - 2*atr(14))
strategy.close('long');
机构常用的波动率目标化仓位:仓位比例 = 风险预算 ÷ 当前 ATR%(截断在 15%–90%)——市场越疯狂仓越轻,平静趋势满仓干。入场是普通的 EMA 金叉,但仓位本身成为策略的一部分。注意两个 cross 函数在顶部无条件调用(见下方 gotcha)。
📌 QUICK REFERENCE
Editor shortcuts
• Ctrl+S — check syntax · Ctrl+Enter — apply to trader
• Tab — autocomplete / indent · ▶ APPLY — send without closing editor
Common gotchas
• crossover()/crossunder() track state per call-site in call order. Always call them unconditionally at the top (upX = crossover(a,b);) and use the variable — a call skipped by && short-circuit or an if branch shifts the state slots and corrupts every later cross.
• strategy.entry is idempotent — won't stack. Use strategy.add for pyramiding.
• strategy.add fires every tick unless guarded. In CRYPTO mode use if (bar_changed) { strategy.add(...) } so it fires once per candle (1m/5m…), not every 200ms.
• SL/TP are fixed at the moment the order is placed — they don't trail automatically.
• SL/TP with wrong-side prices are silently rejected with a toast warning.
• ratio and qty: if both set, qty wins. Omit both → 100% of free cash.
• bb() and stoch() return objects — assign to a variable first: b = bb(20); b.upper.
• All names are case-sensitive: close ✓ Close ✗
Choosing the right tool
| Need | Use |
|---|---|
| Standard indicator | rsi() atr() bb() etc — no state needed |
| Previous bar value | this.prev = value pattern |
| One entry per signal | strategy.entry() |
| Scale into position | if (bar_changed) { strategy.add() } — once per candle |
| Compound gains (roll) | strategy.close() then strategy.entry() |
| Emergency exit | strategy.closeAll() |
| Check if in position | strategy.position.side |
| Limit trade frequency | bar_index % N === 0 or this.cooldown |