Profit Scanner lets you write your own screener criteria rather than picking from a fixed set of dropdowns. The same formula language drives Screener, Signal Alerts and the Formula Evaluator, so a condition written once can be reused in all three.
This article is the complete reference for that language. It applies equally to US equities and to crypto pairs, except where a section says otherwise.
Boolean Expression
Screener criteria are written as a mathematical formula called a Boolean Expression (BE) — a logical statement the system evaluates to one of two values: true or false.
Examples:
4 == 4 ==> true
4 == 3 + 1 ==> true
4 > 5 ==> false
true and false ==> false
true or false ==> true
4 == 4 and 4 == 3 + 1 ==> true
==is the operator for testing equality of values.
The screener evaluates your Boolean Expression against every symbol it covers, then shows those that return true.
For example:
prev hourly close <= current hourly close
Returns stocks whose close on the previous hourly candle is less than or equal to the close on the current hourly candle.
prev hourly close <= current hourly close AND hourly close >= hourly sma("close", 5)
The same criterion, AND close above the hourly MA5.
Screener criteria are flexible. They can be as simple as:
open < close
or as involved as:
hourly high > prev hourly hhv("high", 100) and
hourly close > hourly open and
index macd_histogram > 0 and
high < bollinger_top
Expression hierarchy
Boolean Expressions and Arithmetic Expressions are both derived from Expression. An Arithmetic Expression is a statement that produces a single value in the set of real numbers. Expression itself is defined as “anything that produces a single value”.
The hierarchy is as follows:
Expression :
- Boolean Expression (BE)
- Boolean Value : true, false
- Bracket Expression : ( BE )
- Boolean Operation : and, or, not
- Arithmetic Comparison : >= , <= , > , < , == , !=
- Cross Operation : E cross_up E, E cross_down E
- Arithmetic Expression (E)
- Numeric Expression
- Arithmetic Operation : * , / , + , -
- Bracket Expression : ( E )
- Stock Expression
In this document and in the criteria composer,
Eis shorthand for Arithmetic Expression.
Number notation
Numeric values can be written with a unit suffix, so large numbers stay short and readable:
K = thousand 1,000
M = million 1,000,000
B = billion 1,000,000,000
T = trillion 1,000,000,000,000
Examples:
value > 1B ==> turnover above 1 billion
market_cap >= 10T ==> market capitalisation of at least 10 trillion
volume > 500K ==> volume above 500 thousand
Writing the number in full still works — 1B and 1000000000 are the same value.
Conditional operator
The ? : operator picks one of two values based on a condition:
<condition> ? <value if true> : <value if false>
It is most useful inside a subformula, because it turns a condition into a number that can then be summed or averaged.
sum("close > open ? 1 : 0", 20) ==> how many green candles in 20 bars
sma("volume > 1B ? 1 : 0", 10) ==> proportion of high-volume days
sum("close > prev close ? volume : 0", 5) ==> volume on up days only
To simply count how many times a condition holds, the count function is shorter:
count("close > open", 20) ==> equivalent to the first example above
Stock Expression
The most important part of a screener criterion is the Stock Expression, which is derived from Arithmetic Expression.
Its structure is:
E: stock_expression: [candle_selector] [timeframe] [target] <stock_attr>
E: stock_expression: [candle_selector] [timeframe] [target] <stock_function>
E: stock_expression: <stock_fundamental_attr>
E: stock_expression: <bid_offer_variables>
candle_selector
candle_selector: prev_N, ..., prev_3, prev_2, prev, current
default: current

prev_N can also be written without the underscore: prev2 close is the same as prev_2 close.
A whole formula on an earlier bar: prev()
prev placed before a name moves that one value back a bar. To move a whole formula back, put it in brackets after prev:
prev(close - open) ==> the body of the previous candle
prev(sma(20) * 1.02) ==> the same as prev sma(20) * 1.02
prev(close > open) ==> true when the previous candle was green
prev_3(close) ==> the close three bars back, the same as prev_3 close
prev3(high - low) ==> the range of the candle three bars back
prev(prev(close)) ==> the same as prev_2 close
Every value inside the brackets moves back together, each on its own timeframe: prev(weekly close > weekly open) asks whether last week’s candle was green.
prev() takes a single formula. For more than one bar back, put the number in the name: prev_3(close), not prev(close, 3). The same thing can also be written as val(formula, N):
val(close - open, 1) ==> the same as prev(close - open)
val(close, 5) ==> the same as prev_5(close)
A plain number has no earlier bar, so prev(20) is an error. To compare against an indicator’s previous value, write prev sma(20).
timeframe
timeframe: yearly, quarterly, monthly, weekly, daily,
4hour, 3hour, 2hour, hourly,
45min, 30min, 15min, 10min, 5min, 3min, 2min, 1min
default: daily
30min close > 30min sma(20) ==> above the MA 20 of the 30-minute chart
2hour rsi(14) < 30 ==> oversold on the 2-hour chart
target
target: stock, sector, index
default: stock
- stock — the chart of the symbol itself. For AAPL and BTCUSDT, target
stockselects the AAPL and BTCUSDT charts. - sector — the chart of the sector the symbol belongs to.
- index — the chart of the broad market index the symbol belongs to.
sector and index depend on a symbol being classified into one. Crypto pairs are not, so on a crypto symbol both targets fall back to the symbol’s own chart.
Writing parameters
Parameters with a default value may be omitted. In a Stock Expression, only stock_attr is required.
current daily stock close == close ==> true
current daily stock close == daily close ==> true
current daily stock close == stock close ==> true
current daily stock close == current daily close ==> true
But when parameters are written, they must follow the order defined above:
stock daily close ==> error
daily current close ==> error
current daily close ==> OK
stock_attr list
open, high, low, close, volume
macd, macd_signal, macd_histogram
ppo, ppo_signal, ppo_histogram, pvo
ao, chaikin_osc, tsi, mass_index, trix
aroon_up, aroon_down, aroon_osc
donchian_up, donchian_down, donchian_mid
rsi, stoch_k, stoch_d, stochrsi, atr, adx, pdx, ndx, cci, uo, roc, william_r, mfi, cmf
up_fractal, down_fractal
alligator_jaw, alligator_teeth, alligator_lips, alligator_ao, alligator_ac
bollinger_top, bollinger_bottom, bollinger_mean, bollinger_bandwidth,
bollinger_percent_b, bollinger_percent_b_avg
value
dto_stochk, dto_stochd, obv, adl, sar
pivot_s1, pivot_s2, pivot_s3, pivot_r1, pivot_r2, pivot_r3
camarilla_h1, camarilla_h2, camarilla_h3, camarilla_h4
camarilla_l1, camarilla_l2, camarilla_l3, camarilla_l4
mid_price, hlc3, ohlc4, avg_price, freq, change_percent
up_fractal_index, down_fractal_index
mfv
projected_volume, projected_value
time_progress
open_date, open_time, close_date, close_time
Short forms with a standard period
The indicator names above can be written without brackets. The short form uses that indicator’s standard period, so ppo is the same as ppo(12, 26) and ao the same as ao(5, 34).
| Short form | Standard period |
|---|---|
rsi, atr, adx, pdx, ndx, cci, mfi, william_r | 14 |
roc | 12 |
cmf | 21 |
trix | 15 |
stochrsi | 14 |
dto_stochk, dto_stochd | 14, 8, 5, 3 |
stoch_k, stoch_d | 15, 3, 3 |
uo | 7, 14, 28 |
macd, ppo, pvo | 12, 26 |
macd_signal, macd_histogram, ppo_signal, ppo_histogram | 12, 26, 9 |
ao | 5, 34 |
chaikin_osc | 3, 10 |
tsi | 25, 13 |
mass_index | 9, 25 |
aroon_up, aroon_down, aroon_osc | 25 |
donchian_up, donchian_down, donchian_mid | 20 |
bollinger_* | 20, 2 |
sar | 0.02, 0.2 |
supertrend, supertrend_dir | 10, 3 |
chandelier_long, chandelier_short, chandelier_dir | 22, 3 |
When you need a different period, write the full function form:
rsi < 30 ==> uses the standard period, rsi(14)
rsi(7) < 30 ==> a period of your own choosing
Moving averages also have a numbered form — the function name followed by the period:
sma20 ==> the same as sma("close", 20)
ema50, wma10, aema20, dema20, tema50, vwma20
Shorthand names
The names typed most often also have a shorthand, and a few accept the name used on TradingView or AmiBroker:
| Shorthand | Full name |
|---|---|
xu, crossover | cross_up |
xd, crossunder | cross_down |
dtok, dtod | dto_stochk, dto_stochd |
stok, stod | stoch_k, stoch_d |
pdi, mdi | pdx, ndx |
macdh | macd_histogram |
bbtop, bbbot | bollinger_top, bollinger_bottom |
willr, wpr | william_r |
vol | volume |
hl2 | mid_price |
A shorthand works anywhere its full name does, with parameters, a timeframe or prev:
stok(14, 3) xu stod(14, 3, 3)
weekly dtok(14, 14, 3, 3) > weekly dtod(14, 14, 3, 3)
vol > 2 * sma("volume", 20)
close > bbtop and prev rsi < 30
Underscores are optional. Every name that contains an underscore can also be written without it, which saves a keystroke on a phone keyboard:
changepercent > 2 ==> the same as change_percent > 2
macdhistogram > 0 ==> the same as macd_histogram > 0
bollingertop > close ==> the same as bollinger_top > close
stock_attr definitions
open,high,low,close,volume— the open, high, low, close and volume of the candlestick chartmacd,macd_signal,macd_histogram— values from the MACD(12,26,9) chart;macd_histogram=macd−macd_signalrsi— value from the RSI(14) chartstoch_k,stoch_d— values from the Stochastic(15,3,3) chartstochrsi— value from the StochRSI(9,6) chartatr— value from the ATR(14) chartadx,pdx,ndx— values from the ADX(14) chart;pdxis the DI+ line andndxthe DI− linecci— value from the CCI(14) chartuo— value from the UO(7,14,28) chartroc— value from the ROC(12) chartwilliam_r— value from the WilliamsR(14) chartmfi— value from the MFI(14) chartcmf— value from the CMF(21) chartalligator_jaw,alligator_teeth,alligator_lips,alligator_ao,alligator_ac— values from the Alligator(13,8,5) chartbollinger_top,bollinger_bottom,bollinger_mean,bollinger_bandwidth,bollinger_percent_b,bollinger_percent_b_avg— values from the Bollinger(20,2) chartvalue— total turnover on that bar, i.e. price multiplied by volumeadl— Accumulation/Distribution Line, a running total of money flow volume: the position of the close within the daily range, multiplied by volume. It rises when closes sit near the high and falls when they sit near the lowdto_stochk,dto_stochd— values from the DTOStoch(8,5,3) chartsar— value from the SAR(0.02,0.2) chartmid_price—mid_price= (high+low) / 2; also writtenhl2hlc3— (high+low+close) / 3, the typical price; the same value asvwapohlc4— (open+high+low+close) / 4vwap—vwap= (high+low+close) / 3avg_price— the average transaction price on that bar;avg_price=value/volumefreq— the number of transactions on that barchange_percent— price change against the previous close, as a percentagecamarilla_h1…camarilla_h4,camarilla_l1…camarilla_l4— Camarilla pivot levelsup_fractal_index,down_fractal_index— bars elapsed since the last upper or lower fractal formedmfv— money flow volume, the position of the close within the daily range multiplied by volumeprojected_volume,projected_value— estimated volume and transaction value through to the session close, based on the pace of trading so fartime_progress— the proportion of the trading session elapsed, 0 to 1. Meaningful for equities; crypto trades continuously, so there is no session to measure it againstopen_date,close_date,open_time,close_time— the opening and closing date and time of the bar
A note on
adl. It is cumulative, counted from the oldest bar available, so its value is relative: it cannot be compared between symbols, nor stored and compared again on a different day. What carries meaning is the direction — against the previous bar, or against an average of the value itself.
adl > prev adl ==> accumulation increased today
adl > sma("adl", 20) ==> accumulation above its own average
prev adl < prev sma("adl", 20) and adl > sma("adl", 20)
stock_function list
A stock_function is a function of a stock_attr.
sma(n), sma(subformula,n)
ema(n), ema(subformula,n)
aema(n), aema(subformula,n)
med(n), med(subformula,n)
highest(subformula,n), hhv(subformula,n)
lowest(subformula,n), llv(subformula,n)
ranking(subformula,n)
ranking_per_sector(subformula,n)
roc(n)
macd(fast_period,slow_period)
macd_signal(fast_period,slow_period,signal_period)
macd_histogram(fast_period,slow_period,signal_period)
sum(subformula,n)
supertrend(atr_period,multiplier), supertrend_dir(atr_period,multiplier)
chandelier_long(period,multiplier), chandelier_short(period,multiplier), chandelier_dir(period,multiplier)
cum(subformula)
since(date)
vwap(n)
vwma(n)
wma(n), wma(subformula,n)
linreg(n), linreg(subformula,n)
slope(n), slope(subformula,n)
linreg_r2(n), linreg_r2(subformula,n)
cci(n)
atr(n)
adx(n)
pdx(n)
ndx(n)
stoch_k(lookback_period,k_period)
stoch_d(lookback_period,k_period,d_period)
bollinger_top(period, multiplier)
bollinger_bottom(period, multiplier)
bollinger_mean(period, multiplier)
bollinger_bandwidth(period, multiplier)
bollinger_percent_b(period, multiplier)
bollinger_percent_b_avg(period, multiplier)
stdev_p(n), stdev_p(subformula,n)
stdev_s(n), stdev_s(subformula,n)
stdev(n), stdev(subformula,n)
rsi(n)
stochrsi(period,dtostoch_period,dtostoch_k_period,dtostoch_d_period)
dto_stochk(period,dtostoch_period,dtostoch_k_period,dtostoch_d_period)
dto_stochd(period,dtostoch_period,dtostoch_k_period,dtostoch_d_period)
mfi(n)
cmf(n)
chaikin_osc(fast_period, slow_period)
aroon_up(period)
aroon_down(period)
aroon_osc(period)
dema(period)
tema(period)
trix(period)
mass_index(ema_period, sum_period)
tsi(long_period, short_period)
donchian_up(period)
donchian_down(period)
donchian_mid(period)
ppo(fast_period, slow_period)
ppo_signal(fast_period, slow_period, signal_period)
ppo_histogram(fast_period, slow_period, signal_period)
pvo(fast_period, slow_period)
ao(fast_period, slow_period)
abs(x)
pow(x, y)
sqrt(x)
exp(x)
ln(x)
log10(x), log(x, base)
ceil(x), ceil(x, decimals)
floor(x), floor(x, decimals)
round(x), round(x, decimals)
trunc(x, decimals)
min(a, b, ...), max(a, b, ...), avg(a, b, ...)
count(subformula, n)
hhvbars(subformula, n)
llvbars(subformula, n)
pivot_high(k), pivot_high(k, n)
pivot_low(k), pivot_low(k, n)
pivot_high_bars(k), pivot_high_bars(k, n)
pivot_low_bars(k), pivot_low_bars(k, n)
trade_book(price)
freq(price)
tick_up(price), tick_down(price)
stock_function definitions
sma("<stock_attr>", N) — simple moving average of a stock_attr over N bars.
sma("close", 5) ==> daily MA5 of close
hourly sma("volume", 20) ==> hourly MA20 of volume
ema("<stock_attr>", N) — exponential moving average of a stock_attr over N bars.
ema("close", 5) ==> daily EMA5 of close
hourly ema("volume", 20) ==> hourly EMA20 of volume
aema("<stock_attr>", N) — an exponential moving average that stays more accurate on limited history. Written exactly like ema.
aema("close", 5) ==> daily EMA5 of close
aema("volume * close", 20) ==> EMA20 of transaction value
Use aema when your criterion uses a long period or works on sharply fluctuating values — volume, price differences, ratios. For short periods on price, the two give practically identical results.
If you already have screeners or alerts running on
ema, there is no need to change them.emaremains available and its behaviour is unchanged.
highest("<stock_attr>", N) / hhv("<stock_attr>", N) — the highest value of a stock_attr over N bars.
daily hhv("high", 5) ==> 5 day highest high daily
hourly hhv("close", 5) ==> 5 hour highest close hourly
lowest("<stock_attr>", N) / llv("<stock_attr>", N) — the lowest value of a stock_attr over N bars.
daily llv("low", 5) ==> 5 day lowest low daily
hourly llv("close", 5) ==> 5 hour lowest close hourly
ranking("<stock_attr>", N) — the rank of a stock_attr across all stocks, where rank 1 is the highest value. To order from lowest to highest instead, multiply the stock_attr by −1.
ranking("value") <= 10 ==> the top 10 stocks by transaction value
ranking("roc") <= 10 ==> the 10 stocks with the highest ROC
ranking("roc * -1") <= 10 ==> the 10 stocks with the lowest ROC
roc(period) — value from the Rate of Change (ROC) chart.
roc(12) ==> value of roc(12)
macd(fast_period, slow_period), macd_signal(...), macd_histogram(...) — values from the macd, macd_signal and macd_histogram charts.
macd(12, 26) ==> value of macd(12, 26)
macd_signal(12, 26, 9) ==> value of macd signal(12, 26, 9)
macd_histogram(12, 26, 9) ==> value of macd histogram(12, 26, 9)
sum("<stock_attr>", N) — the sum of a stock_attr over N bars.
sum("mfv", 5) ==> total money flow volume over the last 5 bars
cum("<expression>") — the sum of an expression across every bar available, the equivalent of Cum() in AmiBroker or MetaStock.
cum("volume") ==> accumulated volume across all data
cum("close > open ? 1 : 0") ==> the number of green candles across all data
Because each calculation reads only a bounded stretch of recent bars, the total starts from the oldest bar available rather than from the first bar the symbol ever traded. Its value is therefore relative: it cannot be compared between symbols, nor stored and compared again on a different day — the same caveat that applies to adl.
For most purposes sum with an explicit period is the more useful of the two, since its result is comparable between stocks:
sum("volume", 20) ==> total volume over the last 20 bars
since("<date>") — the number of bars from that date up to the current bar. It is rarely used on its own; its purpose is to stand in for the period argument of another function, so that a calculation is bounded by a date rather than by a bar count.
The date can be written in several forms:
since("2026/01/01") ==> YYYY/MM/DD
since("01/01/2026") ==> DD/MM/YYYY
since("20260101") ==> YYYYMMDD
A hyphenated form such as 2026-01-01 is not recognised.
Examples:
sum("volume", since("2026/01/01")) ==> accumulated volume year to date
hhv("high", since("2026/01/01")) ==> the highest price year to date
llv("low", since("2026/07/01")) ==> the lowest price since the start of July
count("close > open", since("2026/01/01")) ==> green candles year to date
If the date requested is older than all the data held, the result is the number of bars available — the calculation stops at the oldest bar rather than at the date you wrote.
Because
sincecan return a large number, pair it with functions that have no tight period limit:sum,sma,hhv,llv,countandstdev. On functions such asemaortrix, a long date range can exceed the accurate period limit and return an empty value. See the accurate period limits table.
vwap(N) — value from the Volume-Weighted Average Price indicator.
vwap(N) = sum("volume * (high + low + close) / 3", N) / sum("volume", N)
vwap(5) ==> vwap over the last 5 days
vwma(N) — Volume Weighted Moving Average, a close-price average weighted by volume. Unlike vwap, the weighting is applied to the close.
vwma(20) ==> 20-period VWMA
close > vwma(20) ==> price above VWMA20
wma(N) and wma(subformula, N) — Weighted Moving Average, an average with rising weights: the newest bar carries weight N, the oldest weight 1. It responds to a change in price faster than sma over the same period.
wma(20) ==> 20-period WMA
wma("volume", 20) ==> 20-period WMA of volume
close > wma(20)
linreg(N) and linreg(subformula, N) — the value of the linear regression line through the last N bars, read at the current bar. That line is the straight line closest to every point in the range, not merely a line joining the first and last.
linreg(20) ==> the 20-bar regression line at the latest bar
close > linreg(20) ==> price sits above its own regression line
slope(N) and slope(subformula, N) — the gradient of that same line, in price units per bar. Positive when the line rises, negative when it falls.
slope(20) > 0 ==> the 20-bar trend is rising
slope(20) > slope(50) ==> the shorter trend is steeper
prev slope(20) < 0 and slope(20) > 0 ==> the gradient has just turned up
Because it is measured in price per bar, slope cannot be compared directly between stocks trading at very different prices. For screening across the market, convert it to a percentage first:
slope(20) / close * 100 > 0.5 ==> rising by more than 0.5% per bar
slope needs at least 2 bars; with a period of 1 the result is empty.
linreg_r2(N) and linreg_r2(subformula, N) — how closely price hugs that regression line, between 0 and 1. A value of 1 means every point sits exactly on the line; a value near 0 means the line explains essentially nothing about the movement.
slope gives the direction and steepness of a trend; linreg_r2 gives how much that trend is worth trusting. The two are complementary: two stocks can share almost the same gradient, one climbing tidily, the other wandering and merely finishing higher.
linreg_r2(20) > 0.7 ==> the last 20 bars follow the line closely
slope(20) > 0 and linreg_r2(20) > 0.7 ==> an orderly uptrend
slope(20) < 0 and linreg_r2(20) > 0.8 ==> a consistent downtrend
Over a completely flat range the result is 0. Like slope, it needs at least 2 bars.
supertrend(atr_period, multiplier), supertrend_dir(atr_period, multiplier) — TradingView’s Supertrend. The line sits below the price while the trend is up and above it while the trend is down; supertrend_dir is 1 in an uptrend and -1 in a downtrend. The parameters are in the order of TradingView’s indicator settings, ATR length then factor: supertrend(10, 3) is “Supertrend 10 3”. (Pine’s ta.supertrend(factor, atrPeriod) takes them the other way round, and its direction is -1 for up.)
supertrend(10, 3) ==> the Supertrend line
supertrend_dir(10, 3) == 1 ==> in an uptrend
supertrend_dir(10, 3) cross_up 0 ==> turned up on this bar (buy)
supertrend_dir(10, 3) cross_down 0 ==> turned down on this bar (sell)
chandelier_long(period, multiplier), chandelier_short(period, multiplier), chandelier_dir(period, multiplier) — the Chandelier Exit, as the widely used TradingView script. The long stop hangs multiplier ATR below the highest close of period bars and only moves up; the short stop sits as far above the lowest close and only moves down. chandelier_dir is 1 after a close above the short stop and -1 after a close below the long stop. A third argument of 0 uses the highest high and lowest low instead of closes.
chandelier_long(22, 3) ==> the stop for a position you hold
close < chandelier_long(22, 3) ==> the stop was hit
chandelier_dir(22, 3) cross_up 0 ==> turned up on this bar (buy)
chandelier_long(22, 3, 0) ==> from the highest high instead of the highest close
atr(period), adx(period), cci(period)
atr(14) ==> value of the atr(14) line
adx(14) ==> value of the adx(14) line
pdx(14) ==> value of the DI+ line in adx(14)
ndx(14) ==> value of the DI- line in adx(14)
cci(20) ==> value of cci(20)
stoch_k(lookback_period, k_period) / stoch_d(lookback_period, k_period, d_period) — values from the Stochastic Oscillator chart.
stoch_k(15, 3) ==> Stochastic %K(15, 3)
stoch_d(15, 3, 3) ==> Stochastic %D(15, 3, 3)
bollinger_*(period, multiplier) — values from the Bollinger Bands chart.
bollinger_top(20, 2) ==> Bollinger Band top line(20, 2)
bollinger_bottom(20, 2) ==> Bollinger Band bottom line(20, 2)
stdev_p(n), stdev_s(n) and stdev(n) — the standard deviation over the last N bars: a measure of how widely values are spread around their average. The larger the number, the more volatile the movement.
Two forms are available, following Excel’s own naming:
stdev_p(20) ==> population standard deviation (same as Excel's STDEV.P)
stdev_s(20) ==> sample standard deviation (same as Excel's STDEV or STDEV.S)
stdev(20) ==> the same as stdev_s(20)
stdev_p is the form Bollinger Bands use, so it holds that:
stdev_p(20) == bollinger_top(20, 1) - bollinger_mean(20, 1)
The difference between the two is small — around 2.6% at period 20 and 0.5% at period 100 — but it widens at short periods, to roughly 12% at period 5. If you are matching results against a spreadsheet, use stdev. If you are recreating Bollinger bands, use stdev_p.
Without a subformula all three read the closing price. With one, they can be applied to any value:
stdev("volume", 20) ==> spread of 20-day volume
stdev("high - low", 20) ==> spread of the daily range
stdev("(close - prev close) / prev close * 100", 20)
==> volatility of daily returns, in percent
Examples:
close > sma("close", 20) + 2 * stdev_p(20)
==> breaking above two standard deviations
stdev("high - low", 20) < stdev("high - low", 60) * 0.7
==> daily range narrowing against the last three months
stdev(20) / sma("close", 20) * 100 < 2
==> volatility below 2% of price
stdev_s and stdev need at least 2 bars; with a period of 1 the result is empty.
rsi(n) — value from the Relative Strength Index chart.
rsi(14) ==> value of the rsi(14) line
stochrsi(...), dto_stochk(...), dto_stochd(...) — values from the StochRSI chart.
stochrsi(14, 8, 5, 3) ==> value of the stochrsi(14, 8, 5, 3) line
dto_stochk(14, 8, 5, 3) ==> the %K line of stochrsi(14, 8, 5, 3)
dto_stochd(14, 8, 5, 3) ==> the %D line of stochrsi(14, 8, 5, 3)
chaikin_osc(fast_period, slow_period) — the Chaikin Oscillator: the difference between two EMAs of the Accumulation/Distribution Line. It measures accumulation and distribution pressure by taking the position of the close within the daily range, multiplied by volume.
Positive values indicate accumulation pressure, negative values distribution. Zero-line crossings are commonly used as a signal.
chaikin_osc(3, 10) ==> Chaikin Oscillator with standard periods
chaikin_osc(5, 20) ==> a slower variant
Example: switching from distribution to accumulation.
prev chaikin_osc(3, 10) < 0 and chaikin_osc(3, 10) > 0
aroon_up(period) and aroon_down(period) — measure how recently the highest or lowest price was reached within the last N periods, not how high the price is.
Values range from 0 to 100:
- 100 means the high (or low) occurred on the current bar.
- 0 means it occurred on the oldest bar in the window.
aroon_up(25) ==> 100 if today is the highest price of the last 25 days
aroon_down(25) ==> 100 if today is the lowest price of the last 25 days
aroon_osc(period) — the difference between the two, aroon_up minus aroon_down. It ranges from −100 to +100; positive values indicate an upward bias.
aroon_osc(25) ==> equivalent to aroon_up(25) - aroon_down(25)
The usual periods are 25 (Chande’s default) and 14.
dema(period) and tema(period) — Double and Triple Exponential Moving Average of the close. Both track price faster than a plain EMA of the same period, making them more responsive to changes in direction.
dema(20) ==> 20-period DEMA
close > tema(50) ==> price above TEMA50
prev close <= prev dema(20) and close > dema(20)
trix(period) — the percentage rate of change of a triple-smoothed price. It oscillates around zero: positive indicates upward momentum, negative downward.
trix(15) ==> 15-period TRIX
prev trix(15) < 0 and trix(15) > 0 ==> momentum turning up
mass_index(ema_period, sum_period) — detects expansion in the price range (the high-to-low distance), which often precedes a reversal. Standard parameters are mass_index(9, 25).
The common pattern is the reversal bulge: the value rises above 27, then falls back below 26.5.
mass_index(9, 25) > 27
prev mass_index(9, 25) > 27 and mass_index(9, 25) < 26.5
tsi(long_period, short_period) — True Strength Index, measuring the strength and direction of momentum on a scale of −100 to +100. Standard parameters are tsi(25, 13).
tsi(25, 13) > 0 ==> net positive momentum
prev tsi(25, 13) < 0 and tsi(25, 13) > 0
donchian_up(period), donchian_down(period) and donchian_mid(period) — the upper band, lower band and midline of the Donchian Channel: the highest and lowest values over the last N bars.
donchian_up(20) ==> highest high of the last 20 bars
donchian_down(20) ==> lowest low of the last 20 bars
donchian_mid(20) ==> the midpoint of the two
The current bar is included, as with
hhvandllv. This meansclose > donchian_up(20)can never be true — to find a breakout, compare against the previous bar usingprev.
close > prev donchian_up(20) ==> upside breakout
close < prev donchian_down(20) ==> downside breakdown
ppo(fast_period, slow_period) — Percentage Price Oscillator, MACD expressed as a percentage. Because the unit is a percentage, values are comparable across stocks trading at very different price levels.
ppo(12, 26) ==> PPO with standard periods
ppo(12, 26) > 0 ==> short-term trend above long-term
prev ppo(12, 26) < 0 and ppo(12, 26) > 0
ppo_signal(fast_period, slow_period, signal_period) and ppo_histogram(fast_period, slow_period, signal_period) — the signal line and histogram for PPO, matching macd_signal and macd_histogram.
The signal line is a moving average of the PPO value itself; the histogram is the difference between the two (ppo minus ppo_signal).
ppo_signal(12, 26, 9) ==> signal line with standard periods
ppo_histogram(12, 26, 9) ==> PPO relative to its signal line
PPO crossing its signal line — equivalent to the histogram turning from negative to positive:
prev ppo_histogram(12, 26, 9) < 0 and ppo_histogram(12, 26, 9) > 0
Momentum strengthening while the trend is still below zero:
ppo(12, 26) < 0 and ppo_histogram(12, 26, 9) > 0
pvo(fast_period, slow_period) — Percentage Volume Oscillator, the same formula applied to volume. Useful for spotting surges or a drying-up of trading activity.
pvo(12, 26) > 0 ==> short-term volume above the long-term average
ao(fast_period, slow_period) — Awesome Oscillator, the difference between two simple moving averages of the median price. Standard parameters are ao(5, 34).
ao(5, 34) ==> Awesome Oscillator value
prev ao(5, 34) < 0 and ao(5, 34) > 0 ==> zero-line crossing
alligator_aois also available as astock_attr, but its value follows the Alligator(13,8,5) chart settings. Useao(5, 34)if you want the standard periods.
Maths functions
These work on plain numbers rather than price series, so they can be used anywhere in a formula.
abs(x) ==> absolute value
pow(x, y) ==> x to the power of y
sqrt(x) ==> square root
exp(x) ==> e to the power of x
ln(x) ==> natural logarithm, base e
log10(x) ==> logarithm base 10
log(x, base) ==> logarithm to a base you choose
ceil(x), ceil(x, d) ==> round up, optionally to d decimal places
floor(x), floor(x, d) ==> round down
round(x), round(x, d) ==> round to nearest
trunc(x, d) ==> truncate decimals without rounding
min(a, b, ...) ==> the smallest of several arguments
max(a, b, ...) ==> the largest
avg(a, b, ...) ==> the average of several arguments
Examples:
abs(close - open) / open * 100 > 3 ==> intrabar move above 3%, up or down
max(high, prev high) > bollinger_top
A note on logarithm naming. There is deliberately no single-argument
log: in some applicationslogmeans the natural logarithm, in others base 10. Writeln(x)for base e,log10(x)for base 10, orlog(x, base)to choose the base yourself.
The natural logarithm is what makes log returns writable, the usual unit of quantitative analysis:
ln(close / prev close) ==> daily log return
sum("ln(close / prev close)", 20) ==> cumulative 20-day log return
stdev("ln(close / prev close)", 20) * sqrt(252) * 100
==> annualised volatility, in percent
ln and log10 return an empty value when their argument is zero or negative, as does sqrt for a negative argument.
med(n) and med(subformula, n) — the median (not the mean) over the last N bars. More resistant to a single extreme bar than sma.
med("volume", 20) ==> 20-day median volume
volume > med("volume", 20) * 3
count(subformula, n) — counts how many times a condition holds over the last N bars.
count("close > open", 20) >= 14 ==> at least 14 green candles out of 20
count("volume > sma(\"volume\", 20)", 10) >= 5
hhvbars(subformula, n) and llvbars(subformula, n) — how many bars have passed since the highest or lowest value occurred within the last N bars. A value of 0 means it occurred on the current bar.
hhvbars("high", 50) == 0 ==> today is the 50-day high
llvbars("low", 20) <= 3 ==> the 20-day low occurred within the last 3 bars
Swing pivots
pivot_high(k) and pivot_low(k) — the price at the most recent confirmed swing point. A bar is a pivot high when its high stands above every bar within K bars to its left and right; a pivot low is the mirror image.
K sets the size of the swing you are looking for. The larger K is, the fewer pivots are found, and the larger the swings they represent.
pivot_high(5) ==> the price at the last swing peak
pivot_low(5) ==> the price at the last swing trough
A pivot can only be confirmed once K further bars have formed, so the value never changes after the fact.
pivot_high_bars(k) and pivot_low_bars(k) — how many bars have passed since that pivot occurred. The smallest possible value is K, matching the confirmation delay above.
pivot_high_bars(5) == 5 ==> the peak has just been confirmed
pivot_high_bars(5) < pivot_low_bars(5) ==> the peak is newer than the trough
All four take an optional second argument: which pivot to read, counting back from the most recent.
pivot_high(5, 1) ==> the last peak, the same as pivot_high(5)
pivot_high(5, 2) ==> the peak before it
That makes two consecutive swings directly comparable:
pivot_high(5, 1) > pivot_high(5, 2) ==> higher high
pivot_low(5, 1) > pivot_low(5, 2) ==> higher low
pivot_high(5, 1) < pivot_high(5, 2) and pivot_low(5, 1) < pivot_low(5, 2)
==> lower high and lower low together
When the requested pivot cannot be found in the available data, the result is empty.
The
up_fractalanddown_fractalvariables do much the same job with K fixed at 2.pivot_high(2)andpivot_low(2)are their closest equivalent — the two differ slightly in how they treat several bars sharing exactly the same high — with the added ability to set K and to step back through earlier pivots.
Accurate period limits
Every calculation reads only a bounded slice of recent bars. The longer the period you ask for, the more of that history it consumes — and past a certain point the result is no longer reliable.
The table below gives the maximum period that still produces an accurate result. Beyond it the function returns an empty value and the stock will not appear in your screening results.
| Function | Safe period | Notes |
|---|---|---|
sma, hhv, llv, sum, ranking | up to 500 | No practical limit |
wma, linreg, slope, linreg_r2 | up to 500 | No practical limit |
stdev_p, stdev_s, stdev | up to 500 | No practical limit |
pivot_high, pivot_low, and the _bars variants | K up to 100 | The larger K is, the fewer pivots are found |
aroon_up, aroon_down, aroon_osc | up to 500 | No practical limit |
donchian_up, donchian_down, donchian_mid | up to 500 | No practical limit |
vwma, vwap, ao | up to 500 | No practical limit |
ema | 120 | Up to 250 when used on price (close, open, high, low) |
aema | 140 | Up to 200 on price; more accurate than ema for volume and ratios |
dema, tema | 160 | |
trix | 75 | Considerably stricter than the other functions |
ppo, pvo | slow_period up to 140 | Up to 200 for ppo; the standard (12, 26) is well inside |
ppo_signal, ppo_histogram | slow + signal up to 128 | The standard (12, 26, 9) is well inside |
chaikin_osc | slow_period up to 60 | The standard (3, 10) is well inside |
mass_index | ema_period up to 100 | The standard (9, 25) is well inside |
tsi | long + short up to 120 | The standard (25, 13) is well inside |
dto_stochk, dto_stochd | RSI and stochastic periods 2–100, %K and %D periods 1–100 | Needs about RSI + stochastic + %K + %D bars of history: 30 for the standard (14, 8, 5, 3) |
These are conservative figures that hold for any kind of input. Where your criterion works on closing prices, ema and aema stay accurate to the longer periods noted in the last column.
For everyday use, the periods people actually reach for — 5, 9, 14, 20, 26, 50, even 100 — sit far inside the limit for every function.
History counts in bars of the timeframe you ask for. On monthly, dto_stochk(14, 8, 5, 3) needs about 30 months, so a stock listed more recently has no monthly value yet — use weekly, or shorter periods.
Crossing: cross_up and cross_down
a cross_up b is true on the bar where a moves above b: on the previous bar a was at or below b, and on this bar it is above. a cross_down b is the reverse. A cross produces a true or false value directly, so it is a criterion in itself and needs no comparison operator.
Either side can be any value — a number, a variable, a function or a whole formula:
rsi(14) cross_up 30 ==> RSI leaves oversold
rsi(14) cross_down 70 ==> RSI leaves overbought
close cross_up sma(20) ==> price crosses above its MA 20
close cross_up sma(20) * 1.02 ==> ...and clears it by 2%
ema(12) cross_up ema(26) ==> EMA golden cross
macd_histogram cross_down 0 ==> MACD crosses under its signal line
weekly dto_stochk(14, 14, 3, 3) cross_up 20
Written out in full, a cross is a comparison on two bars:
close cross_up sma(20)
==> prev close <= prev sma(20) and close > sma(20)
There are shorter ways to write it, and it can also be written like a function:
rsi(14) xu 30 ==> the same as rsi(14) cross_up 30
close xd sma(50) ==> the same as close cross_down sma(50)
cross_up(rsi(14), 30) ==> the same as rsi(14) cross_up 30
crossover(ema(12), ema(26)) ==> the same as ema(12) cross_up ema(26)
crossunder(close, sma(20)) ==> the same as close cross_down sma(20)
To look for a cross on an earlier bar, or count how often it happened, use it with prev or inside count:
prev cross_up(close, sma(20)) ==> the cross happened on the previous bar
count(close cross_up sma(20), 10) >= 2 ==> crossed above at least twice in 10 bars
stock_fundamental_attr
debt_equity, ebitda, ebitda_anl, eps, eps_anl
ev_ebitda, ev_ebitda_anl, market_cap, netprofit, netprofit_anl
pbv, pbv_anl, per, per_anl, revenue, revenue_anl
roa, roa_anl, roe, roe_anl
The _anl suffix means the annualized value; without it, the value comes from the most recent financial report.
debt_equity— Debt to Equity Ratioebitda/ebitda_anl— Earnings Before Interest, Taxes, Depreciation and Amortizationeps/eps_anl— Earning Per Shareev_ebitda/ev_ebitda_anl— Enterprise Multiplemarket_cap— Market Capitalizationnetprofit/netprofit_anl— Net Profitpbv/pbv_anl— Price-to-Book Ratioper/per_anl— Price-Earnings Ratiorevenue/revenue_anl— Revenueroa/roa_anl— Return on Assetsroe/roe_anl— Return on Equity
Naming parts of a formula: define and var
A long formula is easier to read, and to change, when its parts have names. Write the named parts first, one per line, and finish with the criterion itself:
define body close - open
define spike volume > 2 * sma(volume, 20)
spike and body > 0 and close > sma(close, 20)
This is exactly the same criterion as volume > 2 * sma(volume, 20) and close - open > 0 and close > sma(close, 20). Names work everywhere a formula does: in the Screener, Stock Scoring (end with a number instead of a criterion), Formula-Based Alerts and the Formula Evaluator.
There are two kinds of names.
define: a name for a formula
define name formula gives a formula a name. Wherever the name appears, the formula is worked out right there, on that bar, timeframe or stock. So a define can be used anywhere its formula could:
define body close - open
prev(body) ==> the previous bar's body
prev_3 body ==> the body 3 bars ago
sma(body, 5) ==> the average body over 5 bars
count(body > 0, 10) ==> how many of the last 10 bars rose
body cross_up 0
weekly body ==> this week's body: weekly close - weekly open
code:AAPL body ==> AAPL's body
A define ends at the end of its line. To carry on to the next line, end the line with \; a line that ends inside an open bracket carries on by itself:
define band (hhv("high", 20) +
llv("low", 20)) / 2
close > band
Several defines can share a line, separated by ;. A define can use the defines before it, a name can be defined only once, and a define never changes.
var: a value worked out once
var name = formula; works the formula out once, on the bar being screened, and keeps the result. Unlike a define, a var is one value, the same on every bar. A var can be given a new value later with = (:= works too). Each var line ends with ;:
var lim = sma(close, 50);
var level = lim * 1.02; ==> 2% above the MA 50
level = level * 1.001; ==> and a shade more
close > level
define or var?
The difference shows whenever a name is read on other bars: by prev, val, a cross, or a function that works over several bars, such as sma, count, highest or prev_candle_index.
- A define gives its formula on each of those bars.
- A var gives the same single value on every one of them.
So a name that will be read on other bars must be a define. The screener refuses a var there, rather than return something that looks right and is not:
var m = close - open;
sma(m, 5) > 0
==> m is a var: sma() reads its formula on bar after bar, but a var is one value,
the same on every bar. For a formula read bar by bar, write define m ... instead.
A var is fine as part of a formula that does change from bar to bar: count(close > lim, 10) counts the last 10 bars whose close was above today’s lim.
A rule of thumb: use define for anything that describes a bar (a body, a new high, a crossing), and var for a level worked out today (a target, a threshold, how many bars back something happened).
If you are used to TradingView, note that there every variable is a series; in TQL that is a define. For TradingView’s var (a value carried from bar to bar), see foreach below.
Names and built-in fields
A define or var may take the name of a built-in field, such as define close (high + low) / 2. From that line on, the name means yours. Only the name as you write it changes: built-in indicators such as rsi(14) still use the real close.
Giving a value to a name that was never declared is refused, which also catches a comparison written with a single =:
open = close
==> open is not declared: write var open = ... to make it. To compare, use == (close == open).
Comments
Text after // or # to the end of the line, and anything between /* and */, is ignored, including on the formula’s last line. It is a good place to say what each part is for.
Example: the Darvas box
This TradingView script draws a Darvas box. In Pine, every line is a series:
LL = lowest(low, boxp)
k1 = highest(high, boxp)
k2 = highest(high, boxp - 1)
k3 = highest(high, boxp - 2)
NH = valuewhen(high > k1[1], high, 0)
box1 = k3 < k2
TopBox = valuewhen(barssince(high > k1[1]) == boxp - 2 and box1, NH, 0)
BottomBox = valuewhen(barssince(high > k1[1]) == boxp - 2 and box1, LL, 0)
In TQL, with boxp = 5:
// Darvas box, boxp = 5
define newhigh high > prev highest(high, 5) // a high above the 5 bars before
define formed prev_candle_index(newhigh, 250) == 3 and highest(high, 3) < highest(high, 4) \
and count(volume == 0, 4) == 0 // and the stock traded on those bars
var m = prev_candle_index(formed, 250); // bars since the box formed
var topbox = val(high, m + 3);
var bottombox = val(lowest(low, 5), m);
// a breakout above the box
close > topbox
barssince(cond)isprev_candle_index(cond, 250): the bars since the condition last held, looking back up to 250 bars. (Pine looks back over the whole history.)valuewhen(cond, src, 0)isval(src, prev_candle_index(cond, 250)).newhighandformedare read on earlier bars byprev_candle_index, so they are defines.m,topboxandbottomboxare only needed for today, so they are vars.- When the box forms, the new high is always
boxp - 2bars before it, so the top of the box is simply the highm + 3bars back. count(volume == 0, 4) == 0is not in the Pine script. On days a stock is suspended, the data has a bar at its last price with no volume, where TradingView shows no bar at all. Without this check, a suspension right after a new high looks like a box forming at that high.
Carrying a value from bar to bar: foreach
Some indicators work out each bar from their own value on the bar before. Supertrend’s bands only move one way until a close breaks through them; a trailing stop only moves up. A define cannot do this, because it cannot use itself, and a var is a single value. foreach runs statements on every bar, the oldest first, and vars keep their values from one bar to the next:
var s = 0;
foreach last 10
{
s = s + close; // on each of the last 10 bars
}
s // the same as sum(close, 10)
foreach { ... }runs the statements inside on every bar of the stock’s history, from the oldest to the bar being screened.foreach last N { ... }runs them on the last N bars only.- On each bar, the formulas inside are read on that bar:
closeis that bar’s close,prev closethe one before it,atr(10)the ATR as it was then. - Vars keep their values from one bar to the next. After the loop, a var holds its value at the bar being screened, and the formula after the loop uses it like any other var.
var a, b, c = 1;declares several vars at once. A var declared without a value has none yet:is_nan(x)is true until it is given one. That is how a loop knows it is on its first bar.- Only
varlines and assignments go insideforeach, each ending with;. Put defines before the loop. A loop cannot contain another loop. foreach weekly { ... }runs on weekly bars; any timeframe can be named. A loop reads one timeframe, so a name with another timeframe inside it, such asweekly closein a daily loop, is refused.- A var is still one value, so
prev xon a var is refused inside a loop too. To keep the previous bar’s value, copy it before changing it, asprev_dir = dir;does in the examples below. returnbefore the final formula is optional:return dir == 1is the same asdir == 1.
A loop over the whole history takes longer the first time a formula runs; after that, the values up to the previous bar are kept, and only the latest bar is worked out again. foreach last 250 limits a loop to about a year of daily bars.
Both examples below are also built in: supertrend() and chandelier_long() and the related functions. The loops show how they work, and are a starting point for variations of your own.
Example: Supertrend
Supertrend draws one line: below the price while the trend is up, above it while the trend is down. Its two bands are the bar’s midpoint plus and minus 3 ATR, and each is held in place: the upper band can move down but not up until a close breaks above it, and the lower band the other way round. When a close breaks the band in force, the trend turns.
// Supertrend(10, 3), as TradingView's built-in
var up, dn, prev_dir, dir = -1; // dir: 1 = uptrend, -1 = downtrend
foreach
{
var a = atr(10);
var mid = (high + low) / 2;
var ub = mid + 3 * a; // the bands before they are held
var lb = mid - 3 * a;
up = is_nan(up) or ub < up or prev close > up ? ub : up; // moves down only, until a close above it
dn = is_nan(dn) or lb > dn or prev close < dn ? lb : dn; // moves up only, until a close below it
prev_dir = dir;
dir = dir == -1 and close > up ? 1 : dir == 1 and close < dn ? -1 : dir;
}
dir == 1 and prev_dir == -1 // turned up on this bar
Change the last line for other uses:
| Last line | Gives |
|---|---|
dir == 1 and prev_dir == -1 | turned up on this bar (buy) |
dir == -1 and prev_dir == 1 | turned down on this bar (sell) |
dir == 1 | in an uptrend |
dir == 1 ? dn : up | the Supertrend line itself, for Stock Scoring, the Formula Evaluator or a chart |
Example: Chandelier Exit
Chandelier Exit hangs a long stop 3 ATR below the highest close of the last 22 bars, and a short stop 3 ATR above the lowest close. Each stop is held: the long stop only moves up while closes stay above it, and the short stop only moves down while closes stay below it. The trend turns up when a close crosses above the short stop, and down when a close crosses below the long stop.
// Chandelier Exit(22, 3), on closes
var long_stop, short_stop, prev_dir, dir = 1;
foreach
{
var a = 3 * atr(22);
var ls = highest(close, 22) - a; // the stops before they are held
var ss = lowest(close, 22) + a;
var lp = is_nan(long_stop) ? ls : long_stop; // yesterday's stops
var sp = is_nan(short_stop) ? ss : short_stop;
long_stop = prev close > lp ? max(ls, lp) : ls; // moves up only, while the close stays above it
short_stop = prev close < sp ? min(ss, sp) : ss; // moves down only, while the close stays below it
prev_dir = dir;
dir = close > sp ? 1 : close < lp ? -1 : dir;
}
dir == 1 and prev_dir == -1 // turned up on this bar
This follows the widely used Chandelier Exit script on TradingView with its default of using closes for the highest and lowest. To use highs and lows instead, write highest(high, 22) and lowest(low, 22). The last line works the same way as in Supertrend; long_stop alone gives the stop level for a position you hold.
Building your own formulas
Beyond the built-in functions, you can assemble calculations yourself. The key: sma, ema, aema, sum, hhv, llv and ranking accept a subformula that is a full expression, not merely a stock_attr name. Look at the definition of vwap above — it contains sum("volume * (high + low + close) / 3", N), a complete expression inside quotes.
This section shows how that works, using several widely known indicators.
Every indicator in this section is now available as its own function. For everyday use, call the function directly — it is shorter, faster, and less prone to typos. The long forms are kept here as worked examples, so you can apply the same pattern to indicators that do not yet have one.
Donchian Channel
The upper and lower Donchian bands are the highest and lowest values over the last N bars:
hhv("high", 20) ==> upper band
llv("low", 20) ==> lower band
(hhv("high", 20) + llv("low", 20)) / 2 ==> midline
Available directly as donchian_up(20), donchian_down(20) and donchian_mid(20).
VWMA — Volume Weighted Moving Average
A close-price average weighted by volume. The pattern matches the definition of vwap:
sum("volume * close", 20) / sum("volume", 20)
Available directly as vwma(20).
PPO and PVO
MACD expressed as a percentage, and the same measure applied to volume:
(ema("close", 12) - ema("close", 26)) / ema("close", 26) * 100
(ema("volume", 12) - ema("volume", 26)) / ema("volume", 26) * 100
Available directly as ppo(12, 26) and pvo(12, 26).
Awesome Oscillator
The difference between two simple moving averages of the median price. Since mid_price is already (high + low) / 2, this becomes:
sma("mid_price", 5) - sma("mid_price", 34)
Available directly as ao(5, 34).
Crossover and breakout signals
The signals people reach for — golden cross, death cross, breakout — are fundamentally a comparison between the previous bar and the current one. Writing them yourself lets you choose your own periods, and keeps the criterion readable.
The pattern is always the same: the condition did not hold on prev, and holds now — which is exactly what cross_up and cross_down check (see Crossing above).
Golden cross and death cross, MA 5–20
sma("close", 5) cross_up sma("close", 20) ==> golden cross
sma("close", 5) cross_down sma("close", 20) ==> death cross
Replace 5 and 20 with whatever periods suit your strategy, or swap sma for ema or aema.
MACD crossing the zero line
macd(12, 26) cross_up 0 ==> golden cross
macd(12, 26) cross_down 0 ==> death cross
MACD crossing its signal line — equivalent to the histogram changing sign
macd(12, 26) cross_up macd_signal(12, 26, 9)
macd_histogram(12, 26, 9) cross_down 0
Stochastic %K crossing %D
stoch_k(15, 3) cross_up stoch_d(15, 3, 3)
stoch_k(15, 3) cross_down stoch_d(15, 3, 3)
The same with the shorter names: stok(15, 3) xu stod(15, 3, 3).
Fractal breakout
high > up_fractal and prev close <= up_fractal ==> breaking the upper fractal
low < down_fractal and prev close >= down_fractal ==> breaking the lower fractal
Confirming across many moving averages at once
The ma_net_buy_sell_signal_count variable nets buy against sell signals across 12 moving averages, ranging from −12 to +12.
ma_net_buy_sell_signal_count >= 6 ==> most moving averages strengthening
ma_net_buy_sell_signal_count <= -6 ==> most weakening
Fibonacci retracement
There is no single fib() function in the app, and that is deliberate. A Fibonacci result depends entirely on which points are used as anchors: two people drawing lines on different swings get different numbers, and both are legitimate. So the anchors are yours to choose.
The arithmetic itself is simple. Once a peak and a trough are settled on:
peak - (peak - trough) * ratio ==> up swing, retracement measured down from the peak
trough + (peak - trough) * ratio ==> down swing, retracement measured up from the trough
The ratios in common use are 0.236, 0.382, 0.5, 0.618 and 0.786. The 0.5 is not a Fibonacci ratio at all, merely the midpoint, but it has long since become conventional.
Anchored on swing pivots
The closest thing to the line you would draw yourself on a chart:
pivot_low_bars(5) > pivot_high_bars(5)
? pivot_high(5) - (pivot_high(5) - pivot_low(5)) * 0.618
: pivot_low(5) + (pivot_high(5) - pivot_low(5)) * 0.618
The ? : operator settles the direction of the swing. If the trough is older than the peak, price has just climbed, so the retracement is measured down from the peak.
Anchored on an N-bar range
Simpler, using the highest and lowest price over the last N bars:
llvbars("low", 60) > hhvbars("high", 60)
? hhv("high", 60) - (hhv("high", 60) - llv("low", 60)) * 0.618
: llv("low", 60) + (hhv("high", 60) - llv("low", 60)) * 0.618
Screening for a stock that touched a level
low <= pivot_high(5) - (pivot_high(5) - pivot_low(5)) * 0.618
and close > pivot_high(5) - (pivot_high(5) - pivot_low(5)) * 0.618
==> price dipped through the 61.8% level during the day, then closed back above it.
A shortcut: how far price has already retraced
For screening, what is usually wanted is not the level itself but how far price has retraced within its swing. That number is already available through stoch_k with a smoothing period of 1:
stoch_k(60, 1) ==> where price sits within the 60-bar range, as a percentage
On an up swing, a retracement of R means stoch_k reads 100 − R:
| Retracement | stoch_k(N, 1) |
|---|---|
| 23.6% | 76.4 |
| 38.2% | 61.8 |
| 50.0% | 50.0 |
| 61.8% | 38.2 |
| 78.6% | 21.4 |
So “retraced close to 61.8% of the 60-bar swing” is simply:
stoch_k(60, 1) > 35 and stoch_k(60, 1) < 42
Patterns with no function
Some calculations have no function name but are still easy to write with the same pattern.
Distance from a moving average — useful for screening out stocks that have run too far from their average.
(close - sma("close", 20)) / sma("close", 20) * 100 > 5
Volume spike
volume > sma("volume", 20) * 2
The average of an expression — anything you can write as a single-bar expression, you can average.
sma("(high - low)", 14) ==> average daily range
sma("(close - open) / open * 100", 5) ==> average intrabar change
sum("mfv", 5) ==> 5-bar money flow volume
Ratios across timeframes
volume / weekly volume ==> daily volume as a share of weekly
close / weekly sma("close", 4) ==> price against the 4-week average
Screener formula examples
Higher high higher low
prev high < high and prev low < low
Inside bar
prev high > high and prev low < low
Golden cross MA 5-20 (the MA 5 line crossing above MA 20)
prev sma(5) < prev sma(20) and sma(5) > sma(20)
MACD rising
prev macd < macd and macd > 0
Three white soldiers
prev_2 close > prev_2 open
and prev close > prev open
and close > open
The 10 strongest accumulation symbols by money flow volume
ranking("mfv") <= 10
Stochastic %K crossing %D from below
prev stoch_k < prev stoch_d and stoch_k > stoch_d
Stocks that have just set a 25-day high
aroon_up(25) == 100
A strong uptrend by Aroon — the high is recent, the low is old
aroon_up(25) > 70 and aroon_down(25) < 30
Aroon Oscillator crossing up — a turn from down to up
prev aroon_osc(25) < 0 and aroon_osc(25) > 0
A new trend forming, not yet overbought
aroon_up(14) > 70 and aroon_down(14) < 30 and rsi < 70
Accumulation by Chaikin Oscillator, with above-average volume
chaikin_osc(3, 10) > 0
and prev chaikin_osc(3, 10) < 0
and volume > sma("volume", 20)
Accumulation divergence — price sets a new high while accumulation falls away
close == hhv("close", 20) and adl < prev_5 adl
Double confirmation — Chaikin positive alongside a rising A/D line
chaikin_osc(3, 10) > 0 and adl > prev adl
Restricting results to members of a particular index
close > sma("close", 20) and rsi(14) > 50
Liquid stocks with large transaction value — 5-day average above 1 billion
sma("value", 5) > 1B
A combination: price above the weekly SMA26, MACD histogram rising below the centre line, and Stochastic %K below 19 crossing %D from below.
close > weekly sma("close", 26)
and macd_histogram < 0
and prev_2 macd_histogram < prev macd_histogram
and prev macd_histogram < macd_histogram
and stoch_k < 19
and prev stoch_k < prev stoch_d
and stoch_k > stoch_d
Complete screener examples
The section above shows single conditions. This one assembles them into whole screeners, each starting with a turnover floor so the results are symbols you could actually trade.
One thing to be clear about first: a screener filters, it does not predict. It narrows hundreds of symbols down to the few meeting conditions you set yourself. When to enter, when to leave, and where to stop remain your decisions — and that is where the result is actually determined. The formulas below are examples to study and test, not recommendations.
Intraday momentum
Finding symbols moving on volume above their own habit, but not yet run too far:
value > 5M
and projected_volume > sma("volume", 20) * 2
and change_percent > 1
and change_percent < 6
projected_volume is the key piece: it estimates volume through to the end of the session from the pace so far, so “unusual volume” can be detected at ten in the morning rather than only near the close. The change_percent < 6 ceiling discards symbols whose move is already spent.
Breakout with confirmation
For a two- to three-day hold:
value > 5M
and close > prev hhv("high", 20)
and volume > sma("volume", 20) * 2
and slope(50) > 0
and linreg_r2(50) > 0.5
and close > sma("close", 200)
linreg_r2 is what separates a breakout from an orderly base from a breakout out of pure chop. Without that line the two look identical.
A pullback inside an orderly trend
Buying weakness within strength, while waiting for the turn:
value > 5M
and slope(60) > 0
and linreg_r2(60) > 0.6
and close > sma("close", 200)
and rsi < 40
and close > prev close
That last line waits for price to turn back up rather than catching a falling knife.
Making them your own
Every number above — 20, 50, 60, 5M, 40 — is a choice, not a fixture. Change them and test against the symbols you actually follow. A good screener reflects how you read the market, not how someone else does.
Short forms work too. On the one-minute timeframe, a single line finds symbols turning over at least one million in the last minute:
1min value >= 1M
Questions that come up often
These are the things most frequently asked, with their answers.
The formula looks right but the result is always empty
The most common cause: hhv, highest, llv and lowest already include the current bar. So this is almost never true:
close > hhv("close", 20) ==> almost always false
If today really is the highest close in 20 bars, then hhv("close", 20) equals close, which makes close > hhv(...) false. Compare against the earlier bars instead:
close > prev hhv("close", 19) ==> breaking above the previous 19 bars' high
To say “today is the highest of the last 20 bars” there is a shorter form:
hhvbars("close", 20) == 0
The multiplication sign
Multiplication is written with an asterisk *, not the letter x:
volume >= 2 * ema("volume", 5) ==> correct
volume >= 2 x ema("volume", 5) ==> not valid
What the screener scans
US-listed stocks and ETFs, and the crypto pairs the app covers. Indices themselves are not returned as results, although you can reference one from a formula through the index target.
A formula that uses stock_fundamental_attr values will never match a crypto pair, since those have no financial statements. That is a common reason a mixed screener returns equities only.
How often results refresh
The screener refreshes automatically about once a minute while its market is open. Crypto trades continuously, so crypto results keep refreshing outside US market hours; equity results stop moving once the session closes.
If you are still stuck
Send a description of the condition you are looking for to support@profitscanner.xyz. Describe what you want to find rather than the formula, and we will help you write it.