Strategy Builder Reference

Indicator Guide

Every indicator available in the strategy builder’s condition-tree editor, with what it computes and a worked example. Each one boils down to a single number (or a small handful of them) per day, which a condition compares against a constant or another indicator — e.g. RSI(14) < 30 or Close > Rolling Window High(20).

Price & Candle Basics

Open / High / Low / Close / Volume

open, high, low, close, volume

The raw OHLCV fields themselves, registered as pseudo-indicators so a condition leaf can reference them the same uniform way it references any computed indicator. Each takes an offset param: 0 (default) = the current candle, -1 = the previous candle, -2 = two candles ago, and so on (a positive offset would peek at a future candle and is rejected).

value = the open / high / low / close / volume of candle (current + offset)

A condition like Close > Open is simply asking “did today close green”. Close(0) > Close(-1) asks “did today close above yesterday’s close” — the same field at two different offsets.

Candle Body Midpoint

body_mid

The midpoint of the candle's real body -- halfway between open and close (not high and low). Added specifically so a strategy can check whether a candle's body midpoint clears a line, a looser test than requiring the whole candle above it. Takes the same offset param as Close/Open/etc: 0 = current candle, -1 = the previous one.

body_mid = (open + close) / 2   of candle (current + offset)

Open = ₹100, Close = ₹106 → body_mid = (100 + 106) / 2 = ₹103. A condition like body_mid > EMA(20) passes even if the candle’s low dipped under the EMA, as long as the body’s midpoint cleared it. Use body_mid(-1) to test the prior candle’s midpoint.

Trend & Moving Averages

SMA (Simple Moving Average)

sma

The plain average of the closing price over the trailing N days -- every day in the window weighted equally.

sma(period) = average(close, last `period` days)

Last 5 closes: 100, 102, 101, 103, 104

sma(5) = (100+102+101+103+104) / 5 = 102

EMA (Exponential Moving Average)

ema

Like the SMA, but weights recent days more heavily than older ones, so it reacts faster to new price moves. Computed recursively: today's EMA blends today's close with yesterday's EMA.

ema_today = ema_yesterday + α × (close_today − ema_yesterday)
α = 2 / (period + 1)

With period = 3, α = 2/(3+1) = 0.5. If yesterday’s EMA was ₹100 and today closes at ₹106:

ema_today = 100 + 0.5 × (106 − 100) = ₹103

A bigger move today pulls the EMA further than it would pull an SMA, since the SMA treats every day in its window identically.

MA Rising (vs. N bars ago)

ma_risingperiodlookbackma_type

True if the moving average today is higher than it was `lookback` days ago -- a plain two-point comparison used to gate entries on a genuinely rising (not flat or falling) trend line, without needing a generic 'value N bars ago' operand in the condition tree itself.

ma_rising(period, lookback) = MA[today] > MA[today − lookback]

200-EMA today = ₹505; 200-EMA 20 days ago = ₹498. Since 505 > 498, ma_rising(200, 20) = true. If the average had drifted down to ₹495 instead, it would read false even though the average is still a smooth, slowly-moving line.

Never Closed Below MA

stayed_above_maperiodwindowma_type

True only if the close has been at or above the moving average on every single one of the trailing `window` days, today included -- not just today. This is what lets a strategy require price to have held above a line throughout an approach, not just arrived there.

stayed_above_ma(period, window) = close[t] ≥ MA[t] for every t in the last `window` days
DayClose200-EMAAbove?
1506504✓
2505504✓
3503504✗ — breaks the streak
4507505✓
5508505✓

MA Slope % (flatness)

ma_slope_pctperiodlookback

How much the moving average has moved over the last `lookback` days, as a fraction of its own level -- always a positive magnitude (direction is discarded). Small values mean the average is essentially flat, which is how a mean-reversion strategy checks it's in a range rather than a trend.

ma_slope_pct(period, lookback) = |MA[today] − MA[today − lookback]| / MA[today]

50-SMA today = ₹402, 50-SMA 5 days ago = ₹400 → ma_slope_pct(50, 5) = |402 − 400| / 402 ≈ 0.005 (0.5%). A condition ma_slope_pct(50, 5) <= 0.02 passes — the average has barely budged. A trending name where the 50-SMA rose from ₹380 to ₹402 would read ≈ 0.055 and fail.

Because Day 3 dipped below the average, stayed_above_ma(200, 5) reads false on every day through Day 5 as well — the 5-day window still contains that one violation.

Volatility & Momentum

ATR (Average True Range)

atr

A volatility measure: the average size of a candle's true trading range over the last N days, accounting for gaps between yesterday's close and today's high/low.

true_range = max(high − low, |high − prev_close|, |low − prev_close|)
atr(period) = average(true_range, last `period` days)

Yesterday closed at ₹103. Today: High = ₹108, Low = ₹104.

true_range = max(108−104, |108−103|, |104−103|) = max(4, 5, 1) = 5

ATR is just that value averaged over the last `period` days -- a bigger ATR means the stock has been swinging more per day.

RSI (Relative Strength Index)

rsi

A 0-100 momentum oscillator: how much of recent price movement has been gains vs. losses. Above 70 is conventionally 'overbought', below 30 'oversold'.

RS = average_gain / average_loss\nRSI = 100 − 100 / (1 + RS)

Simplified illustration: over a recent stretch, average daily gain on up-days was ₹2, average daily loss on down-days was ₹1.

RS = 2 / 1 = 2 → RSI = 100 − 100/(1+2) = 100 − 33.3 = 66.7

(The live calculation smooths continuously across the strategy’s whole price history using Wilder’s method, not a simple average over a short window -- this is illustrative, not a hand-reproducible exact match.)

Bollinger %B

bollinger_pctbperiodstd_dev

Where today's close sits relative to a volatility band around its moving average -- 0 means sitting on the lower band, 1 means sitting on the upper band, and values outside [0,1] mean price has pushed beyond a band entirely.

mid = SMA(period);  band = std_dev × rolling std-dev
upper = mid + band;  lower = mid − band
%B = (close − lower) / (upper − lower)

SMA = ₹200, rolling std-dev = ₹5, std_dev multiplier = 2 → upper = 210, lower = 190.

Close = ₹205 → %B = (205 − 190) / (210 − 190) = 15/20 = 0.75

75% of the way from the lower band to the upper band -- comfortably inside the bands, leaning toward the top.

ADX / +DI / -DI

adxadxplus_diminus_di

A trend-strength regime signal, direction-agnostic on its own: ADX measures how strongly a stock (or the market) is trending, regardless of up or down. +DI/-DI tell you which direction that trend is in.

ADX conventionally: ≥25 = trending, ≤18 = weak/choppy\nDirection: +DI > -DI = bullish tilt, -DI > +DI = bearish tilt

ADX(14) = 30 with +DI(14) = 28 and -DI(14) = 14 reads as “strongly trending, and the trend is bullish” -- a condition like ADX(14) >= 25 AND +DI(14) > -DI(14) is exactly this check, the same regime gate the hardcoded trend strategies apply market-wide.

Volume

Average Volume

avg_volume

The plain rolling average of traded volume over N days -- the baseline other volume checks compare today against.

avg_volume(window) = average(volume, last `window` days)

Last 5 days’ volumes: 100k, 120k, 90k, 110k, 130k → avg_volume(5) = 110k.

Volume vs. Average Ratio

volume_vs_avg_ratio

Today's volume expressed as a multiple of its own trailing average -- lets a condition say 'volume was at least 1.5x normal' as a plain comparison against a constant.

volume_vs_avg_ratio(window) = volume[today] / avg_volume(window)

Today’s volume = 150k, avg_volume(5) = 110k → ratio = 150/110 ≈ 1.36. A condition volume_vs_avg_ratio(20) >= 1.5 would reject this day (1.36 < 1.5) but accept a day with, say, 170k volume against the same average.

Volume Percentile Rank

vol_pct_rank

Today's volume's rank, as a percentile (0-1), among the trailing N days -- 1.0 means today is the highest-volume day in the window, 0.0 the lowest.

vol_pct_rank(window) = percentile rank of volume[today] within the last `window` days

Last 5 days’ volumes (today last): 100k, 120k, 90k, 110k, 150k. Today’s 150k is the highest of the 5, so vol_pct_rank(5) = 1.0 — a genuinely unusual volume day relative to its own recent history.

Levels & Ranges

Rolling Window High / Low

rolling_high, rolling_low

The highest high (or lowest low) over the trailing N days, as of YESTERDAY -- today's own bar is deliberately excluded. This is what makes a breakout condition possible: a candle's close can never exceed a window that already includes its own high, so the window has to stop at yesterday.

rolling_high(window) = max(high, trailing `window` days, NOT including today)
DayHighLow
110298
2104100
310197
4108103
5105101
6 (today)10399

On Day 6, rolling_high(5) looks at Days 1–5 only → highest high there is Day 4’s 108. A condition Close > Rolling Window High(5) on Day 6 is genuinely asking “did today break above the last 5 days’ ceiling,” not “did today exceed its own high” (which is impossible).

Swing High / Swing Low

swing_high, swing_low

The highest high (or lowest low) over the trailing N days, INCLUDING today -- the mirror pair to Rolling Window High/Low. Meant for level or distance comparisons (e.g. 'how far is price from its recent swing low'), not as a breakout trigger, since today's own bar can never break a level that already contains it.

swing_high(window) = max(high, trailing `window` days, including today)

Using the same table above, swing_high(5) on Day 6 includes Day 6’s own high of 103 in the window (Days 2–6), so it reads 108 (still Day 4’s high, since 103 doesn’t beat it) -- but if today’s high had been 110, swing_high would read 110, today’s own value, making “close > swing_high” impossible by construction.

Range Compression %

range_compression_pct

How tight the recent trading range has been, as a percentage of today's price -- built directly on Rolling Window High/Low. A small value means the stock has been quiet; a large value means it's been swinging widely.

range_compression_pct(window) = (rolling_high(window) − rolling_low(window)) / close

rolling_high(5) = 204, rolling_low(5) = 197, close = 201:

range_compression_pct(5) = (204 − 197) / 201 ≈ 3.5% — tight, a genuine consolidation.

If the same window instead had a high of 230 and a low of 180: (230 − 180) / 201 ≈ 24.9% — too wide to count as consolidating.

MA % Band (upper/lower)

ma_bandperiodpctma_type

A percentage envelope drawn above and below a moving average -- lets a condition check 'is price within X% of this average' as a plain comparison, since the evaluator can't multiply an indicator by a percentage inside a single condition.

ma_band_upper = MA × (1 + pct)\nma_band_lower = MA × (1 − pct)

200-EMA = ₹500, pct = 3.5%:

ma_band_upper(200, 0.035) = 500 × 1.035 = ₹517.50

ma_band_lower(200, 0.035) = 500 × 0.965 = ₹482.50

Pairing this with Rolling Window High/Low (rolling_low(10) >= ma_band_lower(...) and rolling_high(10) <= ma_band_upper(...)) checks that an entire recent base -- not just today’s close -- sits inside the band.

Trading Range (support / resistance / age)

range_metricssupportresistancewidth_pctsupport_touchesresistance_touchesage_dayssupport_entry

Fits a horizontal trading range to the last `window` bars and reports its floor, ceiling, how wide it is, how many times each edge has been tested, and how long the range has held. All outputs are shifted back by `as_of_offset` bars (default 1) so today's bar can't both define the range and trigger an entry off it. `support_entry` is the support price widened by `proximity_pct` -- a ready-made 'price is within X% of the floor' threshold.

support = a clustered low over the window;  resistance = the matching high
width_pct = (resistance − support) / support
*_touches = bars that came within touch_tolerance_pct of that edge
age_days = bars since the range first formed (capped at max_range_age_cap)
support_entry = support × (1 + proximity_pct)

Over the last 20 bars support fits at ₹196, resistance at ₹204, with 4 support touches and 3 resistance touches, formed 60 bars ago. width_pct = (204 − 196) / 196 ≈ 0.041. With proximity_pct = 0.01, support_entry = 196 × 1.01 = ₹197.96, so close <= range_metrics.support_entry fires once price drops back into the bottom 1% of an established, well-tested range.

Swing Zones (clustered S/R)

swing_zonesupportsupport_lowsupport_highsupport_touchessupport_entryresistanceresistance_lowresistance_highresistance_touches

Finds swing highs and lows over `lookback` bars, clusters the nearby ones into price ZONES (a low/high band, not a single line), and returns the strongest major zone below price as support and the strongest above as resistance. Bar-count params, so referencing it at the weekly timeframe on a daily strategy gives weekly-derived levels. Same as_of_offset 1-bar shift and `support_entry` proximity band as Trading Range.

swing points → clusters within cluster_tolerance_pct → zones
keep zones with ≥ min_touches_major touches and ≥ min_swing_prominence_pct prominence
support = nearest qualifying zone below close;  resistance = nearest above
support_low / support_high = that zone's band edges

Two years of weekly swings cluster into a support zone spanning ₹188–₹192 (touched 3 times) and a resistance zone at ₹210–₹214. support reports ₹190, support_low ₹188, support_high ₹192. A leaf like low <= swing_zone.support_high AND close >= swing_zone.support_low asks “did this bar dip into the support zone and hold”.

Candle Shape

Candle Anatomy (wick/body %)

candle_anatomyupper_wick_pctlower_wick_pctbody_pct

Breaks one candle down into its upper wick, lower wick, and real body, each as a fraction of that candle's own high-low range -- lets a condition describe a candle's shape (e.g. 'no bottom wick', 'a real-bodied candle') as a plain comparison against a constant.

range = high − low
upper_wick_pct = (high − max(open, close)) / range
lower_wick_pct = (min(open, close) − low) / range
body_pct = |close − open| / range

Open = ₹100, Close = ₹106, High = ₹107, Low = ₹99 → range = 8.

upper_wick_pct = (107 − 106) / 8 = 0.125 (12.5%)

lower_wick_pct = (100 − 99) / 8 = 0.125 (12.5%)

body_pct = |106 − 100| / 8 = 0.75 (75%) — a solid, mostly-real-bodied candle with small wicks on both ends.

Candle Range vs. Average Ratio

candle_range_vs_avg_ratio

Today's high-low range divided by its own trailing average range -- lets a condition flag an unusually large candle (an expansion or breakout bar) as a plain comparison against a constant.

candle_range_vs_avg_ratio(window) = (high − low) / average(high − low, last `window` days)

Today’s range = ₹7 (High 107, Low 100). Average range over the last 20 days = ₹4.50. Ratio = 7 / 4.5 ≈ 1.56 — today’s candle was more than 50% bigger than its recent average, the kind of bar a condition like candle_range_vs_avg_ratio(20) >= 1.3 is built to catch.

Cross-Instrument

Relative Strength vs. Sector

relative_strengthperiod_days

The stock's own trailing return minus its sector benchmark index's return over the same window -- positive means the stock is outperforming its sector, whichever way the sector itself is going. This is the one indicator that pulls in a second price series (the stock's sector index, mapped in backend/data/sector_benchmark_map.csv); it can be used as an entry/exit leaf or as the concurrency-ranking key.

stock_return = close / close[period_days ago] − 1
benchmark_return = same, on the sector index (dates aligned, forward-filled)
relative_strength = (stock_return − benchmark_return) × 100

Over the last 10 days the stock is up 6% while its sector index (e.g. NIFTY IT) is up 2% → relative_strength(10) = (6 − 2) = +4. A condition relative_strength(10) > 0 keeps only names leading their sector; used as a ranking method it hands the free concurrency slots to the biggest outperformers first.