Skip to content

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.

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.


These read the traded market’s own series.

EMA_fast(close) − EMA_slow(close)
────────────────────────────────
close

Parameters: 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.

line = EMA_fast(close) − EMA_slow(close)
hist = ( line − EMA_signal(line) ) / close

Parameters: 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.

RS = avg_gain / avg_loss (Wilder's smoothing)
RSI = 100 − 100/(1 + RS)
out = RSI/50 − 1

Parameter: 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.

%B = ( close − SMA ) / ( k · σ ) k = 2

Parameter: 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 − 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.

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 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.

TR_t = max( high−low , |high−prev_close| , |low−prev_close| )
ATR = Wilder_window(TR) (an EMA of span 2·window−1)
out = ATR / close

Parameter: 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.

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 window

Parameter: 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.

out = close / max_window(close) − 1

Parameter: 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 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) ) − 1

Parameters: 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 − 1

Parameters: 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.

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− ) / Σ TR

Parameter: 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 holds
out = +1 or −1, flipping only on a close through the opposite band

Parameters: 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.

The standard library runs to forty-odd names. Most of them are not here, and the reasons divide cleanly in two.

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.

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.

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.

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.


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.


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.