Indicators
Every indicator reads market data and emits one number per 15-minute bar. Nothing here holds state between strategies, and nothing looks ahead: a value at bar t is computed from bars ≤ t only.
Parameters are durations, never bar counts — "3h", "7d", "390m". One
number and one unit (m, h, d, w); "6h30m" is not a duration. The
engine converts to bars at the platform’s 15-minute base and rounds to the
nearest whole bar, with a floor of one.
Why the famous numbers move
Section titled “Why the famous numbers move”MACD’s 12/26/9, RSI’s 14, Bollinger’s 20 — those are daily bars. Copied onto a 15-minute base, 12/26/9 becomes three hours, six and a half, and a bit over two: a day-trading strategy wearing a name earned over months.
Neither reading is wrong, but they are different strategies, so the DSL makes you say which you mean. Where a default below looks unfamiliar it is because it was chosen for this base rather than inherited.
The one place the familiar numbers survive is MACD’s defaults, and only because
180m / 390m / 135m lands on exactly 12 / 26 / 9 bars at 15 minutes.
Local sensors
Section titled “Local sensors”These read the traded market’s own series.
EMA_SPREAD — trend
Section titled “EMA_SPREAD — trend”EMA_fast(close) − EMA_slow(close)──────────────────────────────── closeParameters: fast, slow. Defaults 3h, 12h.
Divided by price so it is scale-free: a BTC leg and a SOL leg can share a weight in the same linear combination without one dominating by price alone.
An EMA uses α = 2/(span+1) and yields NaN until it has seen roughly its
span, so a strategy has no signal during warmup rather than a misleading one.
MACD — the trend’s derivative
Section titled “MACD — the trend’s derivative”line = EMA_fast(close) − EMA_slow(close)hist = ( line − EMA_signal(line) ) / closeParameters: fast, slow, signal. Defaults 180m, 390m, 135m
(12 / 26 / 9 bars).
This is the histogram, not the line. The line is already EMA_SPREAD, and
offering it twice would give one signal two spellings. The histogram is the
rate at which the spread is changing: it turns before the line does and crosses
zero exactly when the classic “MACD crossover” fires. A strategy built on the
line and one built on the histogram are a level and its derivative, not
variations of each other.
The signal EMA is taken over the raw spread, before dividing by price. Smoothing the normalised series would make the smoothing depend on price drift rather than on the spread.
RSI — mean reversion
Section titled “RSI — mean reversion”RS = avg_gain / avg_loss (Wilder's smoothing)RSI = 100 − 100/(1 + RS)out = RSI/50 − 1Parameter: window. Default 1d.
Rescaled from [0, 100] to [−1, +1] so every sensor in the DSL shares one
sign convention: negative is short, positive is long, zero is flat. A graph
mixing raw RSI with a spread would need a weight to undo the offset before it
could do anything else.
BOLLINGER_PCT_B — stretch
Section titled “BOLLINGER_PCT_B — stretch”%B = ( close − SMA ) / ( k · σ ) k = 2Parameter: window. Default 1d.
Distance from the mean in standard deviations. Unbounded, unlike the classic
%B which is rescaled to [0, 1] — the DSL wants a signed, centred number,
and clipping is the activation node’s job.
FUNDING_ZSCORE — crowding
Section titled “FUNDING_ZSCORE — crowding”( funding − mean(funding) ) / σ(funding)Parameter: window. Default 7d.
The funding rate says what it costs to hold a position and, by its sign, which side the book is crowded on. Funding settles every eight hours on most venues, so most 15-minute bars carry a zero; the window should span several settlements.
RETURN — momentum
Section titled “RETURN — momentum”ln( close_t / close_{t−window} )Parameter: window. Default 6h.
Log return, unsmoothed. The rawest sensor here, and the one most improved by a
smooth node after it.
The reversion library
Section titled “The reversion library”The six below measure volatility, position in a range, and — the two that a reversion strategy is really built on — whether the market is currently reverting at all.
Every constant you know for these comes from daily bars. ATR’s 14 is fourteen days; at 15m the same integer is three and a half hours. The defaults here are chosen against the 15-minute clock instead of transplanted, which is the same correction the MACD section above describes.
ATR_PCT — volatility including the gaps
Section titled “ATR_PCT — volatility including the gaps”TR_t = max( high−low , |high−prev_close| , |low−prev_close| )ATR = Wilder_window(TR) (an EMA of span 2·window−1)out = ATR / closeParameter: window. Default 1d.
The second and third terms are why this is not just the bar’s range. A market that gaps between bars — every funding print, every equity session boundary on a venue that never closes — has a true range far larger than any single bar shows, and sizing off the visible range levers into exactly the moves that are hardest to exit.
Divided by price, so a BTC leg and a $2 alt are comparable.
DONCHIAN_PCT — where price sits in its channel
Section titled “DONCHIAN_PCT — where price sits in its channel” 2·( close − min_window(low) )out = ──────────────────────────── − 1 max_window(high) − min_window(low)Parameter: window. Default 1d.
+1 at the window high, −1 at the window low, 0 at the midpoint. A
breakout strategy reads the ends; a reversion strategy reads the same number
with the opposite sign — which is why this is published as a position
rather than as a signal. A flat channel gives 0, not a division by zero.
REALIZED_VOL — how much it has been moving
Section titled “REALIZED_VOL — how much it has been moving”r_t = ln( close_t / close_t−1 )out = σ_window(r)Parameter: window. Default 1d.
Per bar, not annualised. Annualising multiplies by √(bars per year),
which is a constant: it changes no ranking, no z-score and no signal, and puts
a number on screen whose size comes from the convention rather than from the
market. Scale it yourself if you want the familiar figure.
Log returns because they are additive across bars, which is what makes a σ over one window comparable with a σ over another.
Note this differs from ATR_PCT: close-to-close versus the full traded range.
The gap between the two is itself informative — a market whose range is wide
but whose closes barely move is being faded intraday.
VARIANCE_RATIO — trending or reverting
Section titled “VARIANCE_RATIO — trending or reverting” Var( q-bar return )out = ───────────────────── − 1 q · Var( 1-bar return )Parameters: window, horizon. Defaults 7d, 2h.
The statistic this library is named for. Lo–MacKinlay, published centred so that zero means “no information” like every other sensor here.
> 0— moves are followed by more of the same. A mean-reversion strategy is standing in front of them.< 0— moves are reversed. A momentum strategy is buying tops.≈ 0— a random walk, which is the null it is measured against.
Drift is not trend, to this statistic. A market grinding steadily upward with independent noise is a random walk with a mean, and its variance still grows linearly — this correctly reads ≈ 0. What it detects is persistence, not slope.
Use it as a gate on another sensor rather than as an exposure of its own: multiply a momentum signal by it, or use it to choose between two branches.
horizon is the q, as a duration — 2h is eight bars at 15m. Estimated from
overlapping windows, so it needs window + horizon bars before it emits
anything.
RETURN_AUTOCORR — reversion, measured directly
Section titled “RETURN_AUTOCORR — reversion, measured directly”out = corr( r_t , r_t−1 ) over the windowParameter: window. Default 1d.
Negative means a move tends to be followed by its opposite. Bounded to [-1, 1]
by construction, so it is safe to wire into a combiner without an activation.
The blunter cousin of VARIANCE_RATIO — one lag rather than a horizon, so it
is noisier and reacts faster. Both exist because they disagree in the
interesting cases: a market can revert bar to bar while trending over the day,
and that combination is a spread trade rather than a directional one.
DRAWDOWN — depth below the recent high
Section titled “DRAWDOWN — depth below the recent high”out = close / max_window(close) − 1Parameter: window. Default 7d.
0 at a new high, −0.2 twenty percent below one. Negative, deliberately:
“worse” should be “more negative” everywhere in the graph. A drawdown published
as a positive number reads as a bullish input to any weight that does not
special-case it, which is the easiest sign error in the DSL to make and the
hardest to see.
This is the market’s drawdown and depends on no position — distinct from the account drawdown the allocator sizes leverage against.
The wider library
Section titled “The wider library”The seven below come from the standard technical-analysis library, restated on the 15-minute clock. Each is here because it measures something the sensors above do not — see What is deliberately missing for the much longer list of names that were considered and left out.
STOCH_RSI — momentum inside its own range
Section titled “STOCH_RSI — momentum inside its own range”out = 2·( RSI − min_stoch(RSI) ) / ( max_stoch(RSI) − min_stoch(RSI) ) − 1Parameters: rsi, stoch. Defaults 3h, 3h.
Not a second RSI. RSI at 55 says little; RSI at 55 after a day spent between 50 and 57 says the market is at the top of its recent momentum range. That second reading is what this measures, and it is why StochRSI turns well before RSI does — and why it spends much more time pinned at its extremes.
A flat RSI has no range to sit inside, so the output is 0 — mid — rather
than a division by a vanishing span.
ULTIMATE_OSC — buying pressure over three horizons
Section titled “ULTIMATE_OSC — buying pressure over three horizons”BP = close − min( low , prev_close )TR = max( high , prev_close ) − min( low , prev_close )out = 2·( 4·Σ(BP)/Σ(TR)|short + 2·…|medium + 1·…|long ) / 7 − 1Parameters: short, medium, long. Defaults 2h, 4h, 8h.
Williams’ answer to the objection that a single-window oscillator changes character with its window. Three nested windows, weighted 4/2/1 toward the shortest.
True range in the denominator, not the bar’s own range — the same reason
ATR_PCT uses it. A measure that ignores gaps is most wrong exactly when the
market moved.
TSI — momentum over its own absolute size
Section titled “TSI — momentum over its own absolute size”out = EMA_fast( EMA_slow( Δclose ) ) / EMA_fast( EMA_slow( |Δclose| ) )Parameters: slow, fast. Defaults 6h, 3h.
The division is what makes it scale-free: the numerator alone is denominated
in dollars and would rank a $60,000 market above a $2 one for the same
proportional move. Bounded in [-1, 1] by construction, because smoothing
|x| always dominates smoothing x.
AROON_OSC — how long since the extremes
Section titled “AROON_OSC — how long since the extremes”out = ( bars_since_low − bars_since_high ) / ( window − 1 )Parameter: window. Default 6h.
The reason to carry it beside DONCHIAN_PCT, which answers the same question
in magnitude. A market pinned just under a high it set an hour ago and one
pinned just under a high it set a week ago read identically on Donchian and
opposite here.
Aroon is ordinal, and ordinal survives a change in volatility regime that a magnitude does not.
VORTEX — which direction is doing the work
Section titled “VORTEX — which direction is doing the work”VM+ = |high_t − low_t−1| VM− = |low_t − high_t−1|out = ( Σ VM+ − Σ VM− ) / Σ TRParameter: window. Default 6h.
Distinct from EMA_SPREAD, which measures where price is relative to its
own average. This measures the character of how it got there: a market can
grind upward with almost no VI separation, and that is a different trade.
SUPERTREND — a trend state, with hysteresis
Section titled “SUPERTREND — a trend state, with hysteresis”band± = (high+low)/2 ± multiple · ATR(window) …ratcheted so a band only tightens toward price while the trend holdsout = +1 or −1, flipping only on a close through the opposite bandParameters: window, multiple. Defaults 3h, 3.
The hysteresis is the whole value. EMA_SPREAD crosses zero repeatedly in a
chop; this does not flip until the market has actually gone somewhere, which
at 15-minute bars is the difference between a signal and a fee schedule.
Output is NaN until the ATR exists — not +1. An early version seeded its
bands while the ATR was still warming, and because every comparison with NaN
is false the ratchet kept its previous band at every bar afterwards: the
sensor returned +1 for the entire series. Plausible for a rising market, and
completely wrong.
KELTNER_PCT — stretch, measured against ATR
Section titled “KELTNER_PCT — stretch, measured against ATR”out = ( close − EMA(window) ) / ( multiple · ATR(window) )Parameters: window, multiple. Defaults 6h, 2.
BOLLINGER_PCT_B answers the same question against standard deviation, and
the two disagree in the case that matters. σ is computed from closes and has
no idea a bar gapped; ATR is built on true range and does.
So in the hours around a liquidation cascade Bollinger reports an extreme stretch while Keltner reports a band that widened to accommodate it — which is the more useful reading when the question is whether to fade the move.
What is deliberately missing
Section titled “What is deliberately missing”The standard library runs to forty-odd names. Most of them are not here, and the reasons divide cleanly in two.
Already present under another name
Section titled “Already present under another name”A second name for a series the engine already computes makes the palette look richer and makes two identical strategies look different — which is precisely what the launch gate’s trial counting exists to price.
| Name | What it actually is |
|---|---|
| Stochastic %K | DONCHIAN_PCT. The same (C−LL)/(HH−LL), already centred. |
| Williams %R | Stochastic %K negated. |
| Rate of Change | RETURN. |
| Price Oscillator (PPO) | EMA_SPREAD, times a hundred. |
| MA crossovers | EMA_SPREAD. A crossover is the sign of a spread. |
| SMA, EMA, RMA, WMA, HMA, ALMA, DEMA, LSMA, McGinley | Not signals at all. |
| Envelope %B | BOLLINGER_PCT_B with a fixed percentage instead of σ. |
That last group deserves a sentence. A moving average on its own is not a
signal: it is priced in dollars, so it cannot be combined with anything else
or compared across markets. Every strategy that uses one uses a spread — and
that is EMA_SPREAD.
Not buildable from the data we carry
Section titled “Not buildable from the data we carry”The feed publishes close, high, low and funding per market. Everything
below needs an input that is not there:
| Name | Needs |
|---|---|
| VWAP, VWMA, VWEMA, VWLMA, VWRMA | volume |
| Elder Force Index | volume |
| Ease of Movement | volume |
| Klinger Oscillator | volume |
| Volume Oscillator | volume |
| Relative Vigor Index | the bar’s open |
These are not refusals on principle — they are waiting on the feed. If volume is added to the export, most of the first group becomes a small change here.
Global sensors
Section titled “Global sensors”These read the whole venue’s cross-section, precomputed by the feed because every strategy on the platform shares them. They take no parameters.
| sensor | what it is |
|---|---|
BENCHMARK_MOM |
the benchmark market’s momentum |
VENUE_FUNDING_BIAS |
funding across the venue: is the whole book long |
MEDIAN_RETURN |
the cross-sectional median return — what “the market” did |
VENUE_BREADTH |
share of markets advancing. Participation, not size |
RELATIVE_STRENGTH |
this market’s return minus the venue median |
RELATIVE_STRENGTH is derived rather than stored — the feed publishes the
median and the engine subtracts the local return, because storing the spread
per market would multiply the export for a value one subtraction away. Its
window is optional and defaults to 24h.
Point-in-time, on purpose
Section titled “Point-in-time, on purpose”The cross-section is computed over the markets listed at that bar, from
market_listings. Using today’s universe would bake in survivorship bias:
markets that die are the ones that did badly, so their absence drags the
historical median upward for every strategy at once — and the launch gate
cannot catch it, because the bias is in the data and inflates the holdout
exactly as much as the training window.
The z-score wrapper
Section titled “The z-score wrapper”Any indicator node may carry "zscore": "7d", which normalises its output over
a rolling window before the value reaches the graph:
( x − mean_window(x) ) / σ_window(x)This is usually what you want. Two sensors on different scales cannot be combined by a weight in any meaningful way, and a z-score puts both in units of their own recent variability. It is also what makes a weight portable between markets.
Warmup
Section titled “Warmup”Every rolling sensor emits NaN until it has enough history. The engine
propagates NaN rather than substituting a zero, because a zero is a flat
position, which is a claim, and the honest answer during warmup is that there
is no signal yet.
The bootstrap’s block length must span the longest window in the strategy — resampling across a boundary shorter than the window destroys the rolling normalisation those sensors are built from.