KDB knowledge

Volatility indicators and bands

How much the price moves: true range, ATR in percent, Bollinger bands, Keltner and Donchian channels.

Volatility is how much the price moves up and down. A share whose price jumps 3% a day is more volatile (more risky) than a share that moves 0.5% a day. Volatility tells you nothing about the direction.

Traders use volatility to:

These functions are series functions, like sma and atr. The same rules apply:

Function (default periods)First value on candleNULL candles at the start
trange(high, low, close)21
natr(high, low, close, n) (n = 14)n + 1n
bb_upper, bb_middle, bb_lower, bb_percent_b, bb_width (x, n) (n = 20)nn - 1
kc_middle(high, low, close, n) (n = 20)nn - 1
kc_upper, kc_lower (high, low, close, n, mult, atr_n) (20, 2, 10)the bigger of n and atr_n + 1one less
dc_upper, dc_middle, dc_lower (high, low, n) (n = 20)nn - 1

Example: Bollinger bands, ATR in percent and the 20-minute channel of CITY (the last 5 minutes) (sample data: sample)

SELECT time, close, lower_band, middle_band, upper_band, natr14, low_20, high_20
FROM (SELECT time, close,
             round(bb_lower(close, 20, 2), 2) AS lower_band,
             round(bb_middle(close, 20, 2), 2) AS middle_band,
             round(bb_upper(close, 20, 2), 2) AS upper_band,
             round(natr(high, low, close, 14), 3) AS natr14,
             dc_lower(high, low, 20) AS low_20,
             dc_upper(high, low, 20) AS high_20
      FROM ochl_minutes
      WHERE symbol = 'CITY') t
ORDER BY time DESC
LIMIT 5;

Result:

time (TIMESTAMP)close (DOUBLE)lower_band (DOUBLE)middle_band (DOUBLE)upper_band (DOUBLE)natr14 (DOUBLE)low_20 (DOUBLE)high_20 (DOUBLE)
2026-09-10T09:14:00.000Z504.9504.1504.97505.840.122503.9506.1
2026-09-10T09:13:00.000Z505.1504.04504.95505.850.12503.9506.1
2026-09-10T09:12:00.000Z504.8504.04504.94505.840.121503.9506.1
2026-09-10T09:11:00.000Z504.5504.03504.94505.840.122503.9506.1
2026-09-10T09:10:00.000Z504.6504.08504.98505.870.13503.9506.1

(5 rows)

trange, true_range

Syntax:

trange(high, low, close)
true_range(high, low, close)
trange(high, low, close) OVER ([PARTITION BY ...] [ORDER BY ...])

The true range of one candle: the biggest of high - low, |high - previous close| and |low - previous close|. It is how far the price really moved in that candle, a gap included (a gap is when a candle opens far away from the previous close). true_range is the same function. The atr is the average of the true range.

ArgumentTypeMeaning
highnumberthe candle's high
lownumberthe candle's low
closenumberthe candle's close (the previous candle's close is used)

Example: A gap counts: COAST on 9 September (sample data: sample)

SELECT time, high, low, prev_close, round(high - low, 2) AS high_minus_low, true_range
FROM (SELECT time, high, low,
             lag(close) AS prev_close,
             round(trange(high, low, close), 2) AS true_range
      FROM ochl_day
      WHERE symbol = 'COAST' AND time >= '2026-09-03') t
ORDER BY time;

Result:

time (TIMESTAMP)high (DOUBLE)low (DOUBLE)prev_close (DOUBLE)high_minus_low (DOUBLE)true_range (DOUBLE)
2026-09-03T00:00:00.000Z373.6364.1NULL9.5NULL
2026-09-06T00:00:00.000Z378.5372.5373.266
2026-09-07T00:00:00.000Z381.1373.2375.47.97.9
2026-09-08T00:00:00.000Z380.2374.7379.25.55.5
2026-09-09T00:00:00.000Z398.8379.2377.219.621.6
2026-09-10T00:00:00.000Z382373.2380.18.88.8

(6 rows)

Example: Plain 5-day average of the true range against Wilder's ATR (sample data: sample)

SELECT time, true_range,
       round(avg(true_range) OVER (ORDER BY time ROWS 4 PRECEDING), 2) AS avg_5,
       atr5
FROM (SELECT time,
             round(trange(high, low, close), 2) AS true_range,
             round(atr(high, low, close, 5), 2) AS atr5
      FROM ochl_day
      WHERE symbol = 'VOLT' AND time >= '2026-08-23') t
ORDER BY time;

Result:

time (TIMESTAMP)true_range (DOUBLE)avg_5 (DOUBLE)atr5 (DOUBLE)
2026-08-23T00:00:00.000ZNULLNULLNULL
2026-08-24T00:00:00.000Z5.15.1NULL
2026-08-25T00:00:00.000Z9.47.25NULL
2026-08-26T00:00:00.000Z6.46.97NULL
2026-08-27T00:00:00.000Z2.95.95NULL
2026-08-30T00:00:00.000Z3.65.485.48
2026-08-31T00:00:00.000Z4.35.325.24
2026-09-01T00:00:00.000Z5.34.55.26
2026-09-02T00:00:00.000Z5.64.345.32
2026-09-03T00:00:00.000Z5.84.925.42
2026-09-06T00:00:00.000Z11.56.56.64
2026-09-07T00:00:00.000Z8.97.427.09
2026-09-08T00:00:00.000Z6.67.686.99
2026-09-09T00:00:00.000Z7.88.127.15
2026-09-10T00:00:00.000Z7.98.547.3

(15 rows)

Example: high, low and close are all needed (sample data: sample; shows an error)

SELECT trange(high, low) FROM ochl_day;

Result:

Error: line 1, column 8: function 'trange' takes 3 argument(s), got 2

natr

Syntax:

natr(high, low, close)
natr(high, low, close, n)
natr(high, low, close, n) OVER ([PARTITION BY ...] [ORDER BY ...])

The normalized ATR: the atr divided by the close, times 100. So it is the ATR in percent of the price. With natr you can compare a 500-dollar share with a 180-dollar share: 1.5 means the price moves about 1.5% in one candle. The same as TA-Lib NATR.

ArgumentTypeMeaning
highnumberthe candle's high
lownumberthe candle's low
closenumberthe candle's close
nwhole number, 1 or morethe ATR period; default 14

Example: Which share moves most? 5-day NATR on the last day (sample data: sample)

SELECT symbol, close, natr5
FROM (SELECT time, symbol, close,
             round(natr(high, low, close, 5) OVER (PARTITION BY symbol ORDER BY time), 2) AS natr5
      FROM ochl_day
      WHERE time >= '2026-08-23') t
WHERE time IN '2026-09-10'
ORDER BY natr5 DESC;

Result:

symbol (SYMBOL)close (DOUBLE)natr5 (DOUBLE)
VOLT191.23.82
COAST376.12.58
CITY504.91.45

(3 rows)

Example: A stop-loss 2 × NATR percent below the close is the same as 2 × ATR (sample data: sample)

SELECT symbol, close, round(natr5, 2) AS natr5,
       round(close * (1 - 2 * natr5 / 100), 1) AS stop_from_natr,
       round(close - 2 * atr5, 1) AS stop_from_atr
FROM (SELECT time, symbol, close,
             natr(high, low, close, 5) OVER (PARTITION BY symbol ORDER BY time) AS natr5,
             atr(high, low, close, 5) OVER (PARTITION BY symbol ORDER BY time) AS atr5
      FROM ochl_day
      WHERE time >= '2026-08-23') t
WHERE time IN '2026-09-10'
ORDER BY symbol;

Result:

symbol (SYMBOL)close (DOUBLE)natr5 (DOUBLE)stop_from_natr (DOUBLE)stop_from_atr (DOUBLE)
CITY504.91.45490.2490.2
COAST376.12.58356.7356.7
VOLT191.23.82176.6176.6

(3 rows)

Example: NATR 14 of COAST's one-minute candles (the last 5 minutes) (sample data: sample)

SELECT time, close, natr14
FROM (SELECT time, close, round(natr(high, low, close), 4) AS natr14
      FROM ochl_minutes
      WHERE symbol = 'COAST') t
ORDER BY time DESC
LIMIT 5;

Result:

time (TIMESTAMP)close (DOUBLE)natr14 (DOUBLE)
2026-09-10T09:14:00.000Z376.10.1513
2026-09-10T09:13:00.000Z376.20.1526
2026-09-10T09:12:00.000Z3760.1522
2026-09-10T09:11:00.000Z375.50.1477
2026-09-10T09:10:00.000Z375.60.1508

(5 rows)

bb_upper, bb_middle, bb_lower

Syntax:

bb_upper(x)    bb_upper(x, n)    bb_upper(x, n, mult)
bb_middle(x)   bb_middle(x, n)   bb_middle(x, n, mult)
bb_lower(x)    bb_lower(x, n)    bb_lower(x, n, mult)
bb_upper(x, n, mult) OVER ([PARTITION BY ...] [ORDER BY ...])

The three Bollinger bands. The middle band is the sma of the last n prices. The upper band is the middle band plus mult × the standard deviation of those prices (a number for how far the prices are from their average); the lower band is the middle band minus the same amount. When the price jumps around, the bands get wide; when the market is quiet, they get narrow.

With mult 2, most closes stay between the bands. A close above the upper band is a strong move up (a breakout), or a sign that the price went too far and may come back. A close below the lower band is the same, downward.

ArgumentTypeMeaning
xnumberthe price, usually close
nwhole number, 1 or morehow many candles; default 20
multnumberhow many standard deviations away; default 2 (2.5 is allowed)

Example: 4-day bands of CITY with mult 1.5: is the close inside? (sample data: sample)

SELECT time, close, lower_band, middle_band, upper_band,
       CASE WHEN upper_band IS NULL THEN NULL
            WHEN close > upper_band THEN 'above'
            WHEN close < lower_band THEN 'below'
            ELSE 'inside' END AS place
FROM (SELECT time, close,
             round(bb_lower(close, 4, 1.5), 2) AS lower_band,
             round(bb_middle(close, 4, 1.5), 2) AS middle_band,
             round(bb_upper(close, 4, 1.5), 2) AS upper_band
      FROM ochl_day
      WHERE symbol = 'CITY' AND time >= '2026-08-23') t
ORDER BY time;

Result:

time (TIMESTAMP)close (DOUBLE)lower_band (DOUBLE)middle_band (DOUBLE)upper_band (DOUBLE)place (STRING)
2026-08-23T00:00:00.000Z507.9NULLNULLNULLNULL
2026-08-24T00:00:00.000Z510.3NULLNULLNULLNULL
2026-08-25T00:00:00.000Z513.3NULLNULLNULLNULL
2026-08-26T00:00:00.000Z513.2507.81511.18514.54inside
2026-08-27T00:00:00.000Z505.9506.16510.68515.19below
2026-08-30T00:00:00.000Z503.1502.15508.88515.6inside
2026-08-31T00:00:00.000Z505.7501.34506.98512.61inside
2026-09-01T00:00:00.000Z502.4501.96504.28506.59inside
2026-09-02T00:00:00.000Z496.4496.79501.9507.01below
2026-09-03T00:00:00.000Z497.9495.08500.6506.12inside
2026-09-06T00:00:00.000Z499495.61498.93502.24inside
2026-09-07T00:00:00.000Z498.7496.49498499.51inside
2026-09-08T00:00:00.000Z497.5497.37498.28499.18inside
2026-09-09T00:00:00.000Z504.6495.84499.95504.06above
2026-09-10T00:00:00.000Z504.9496.39501.43506.46inside

(15 rows)

Example: Breakouts: the minutes where the close crosses above the upper band (20, 2) (sample data: sample)

SELECT time, symbol, close, round(upper_band, 3) AS upper_band
FROM (SELECT time, symbol, close, upper_band,
             lag(close) OVER (PARTITION BY symbol ORDER BY time) AS prev_close,
             lag(upper_band) OVER (PARTITION BY symbol ORDER BY time) AS prev_upper
      FROM (SELECT time, symbol, close,
                   bb_upper(close, 20, 2) OVER (PARTITION BY symbol ORDER BY time) AS upper_band
            FROM ochl_minutes
            WHERE time IN '2026-09-10') a) b
WHERE close > upper_band AND prev_close <= prev_upper
ORDER BY time;

Result:

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)upper_band (DOUBLE)
2026-09-10T05:42:00.000ZCITY505.7505.579
2026-09-10T06:03:00.000ZCOAST380.9380.614
2026-09-10T06:24:00.000ZCITY506.4506.264
2026-09-10T07:26:00.000ZCITY503.1503.087
2026-09-10T07:35:00.000ZCOAST378.8378.617
2026-09-10T07:41:00.000ZCITY503.6503.6
2026-09-10T07:43:00.000ZCITY503.7503.7
2026-09-10T08:21:00.000ZCITY504.2504.179
2026-09-10T08:28:00.000ZCOAST378377.938
2026-09-10T08:39:00.000ZCITY506.3506.298
2026-09-10T08:42:00.000ZCITY506.9506.823

(11 rows)

Example: Wider bands with mult 3 (sample data: sample)

SELECT time, close,
       round(bb_lower(close, 5, 2), 2) AS lower_2,
       round(bb_lower(close, 5, 3), 2) AS lower_3,
       round(bb_upper(close, 5, 2), 2) AS upper_2,
       round(bb_upper(close, 5, 3), 2) AS upper_3
FROM ochl_day
WHERE symbol = 'VOLT' AND time >= '2026-09-03'
ORDER BY time;

Result:

time (TIMESTAMP)close (DOUBLE)lower_2 (DOUBLE)lower_3 (DOUBLE)upper_2 (DOUBLE)upper_3 (DOUBLE)
2026-09-03T00:00:00.000Z176NULLNULLNULLNULL
2026-09-06T00:00:00.000Z180.1NULLNULLNULLNULL
2026-09-07T00:00:00.000Z176.9NULLNULLNULLNULL
2026-09-08T00:00:00.000Z182.4NULLNULLNULLNULL
2026-09-09T00:00:00.000Z187.3172.38168.3188.7192.78
2026-09-10T00:00:00.000Z191.2173.38168.28193.78198.88

(6 rows)

Example: mult must be a number (sample data: sample; shows an error)

SELECT bb_upper(close, 20, 'two') FROM ochl_day;

Result:

Error: line 1, column 8: bb_upper: the factor must be a number, got 'two'

bb_percent_b

Syntax:

bb_percent_b(x)
bb_percent_b(x, n)
bb_percent_b(x, n, mult)
bb_percent_b(x, n, mult) OVER ([PARTITION BY ...] [ORDER BY ...])

%B ("percent B") says where the price is between the Bollinger bands: (x - lower band) / (upper band - lower band).

%BThe price is
more than 1above the upper band
1on the upper band
0.5on the middle band
0on the lower band
less than 0below the lower band
ArgumentTypeMeaning
xnumberthe price, usually close
nwhole number, 1 or morehow many candles; default 20
multnumberhow many standard deviations; default 2

Example: Where is VOLT inside its 5-day bands? (sample data: sample)

SELECT time, close, pct_b,
       CASE WHEN pct_b > 1 THEN 'above the bands'
            WHEN pct_b < 0 THEN 'below the bands'
            WHEN pct_b >= 0.5 THEN 'upper half'
            ELSE 'lower half' END AS zone
FROM (SELECT time, close, round(bb_percent_b(close, 5), 3) AS pct_b
      FROM ochl_day
      WHERE symbol = 'VOLT' AND time >= '2026-08-23') t
ORDER BY time;

Result:

time (TIMESTAMP)close (DOUBLE)pct_b (DOUBLE)zone (STRING)
2026-08-23T00:00:00.000Z181NULLlower half
2026-08-24T00:00:00.000Z178.5NULLlower half
2026-08-25T00:00:00.000Z176.8NULLlower half
2026-08-26T00:00:00.000Z174.2NULLlower half
2026-08-27T00:00:00.000Z173.30.192lower half
2026-08-30T00:00:00.000Z1730.248lower half
2026-08-31T00:00:00.000Z172.60.271lower half
2026-09-01T00:00:00.000Z1690.022lower half
2026-09-02T00:00:00.000Z171.50.439lower half
2026-09-03T00:00:00.000Z1760.895upper half
2026-09-06T00:00:00.000Z180.10.906upper half
2026-09-07T00:00:00.000Z176.90.639upper half
2026-09-08T00:00:00.000Z182.40.837upper half
2026-09-09T00:00:00.000Z187.30.914upper half
2026-09-10T00:00:00.000Z191.20.874upper half

(15 rows)

Example: %B of both symbols at the last minute (20, 2) (sample data: sample)

SELECT symbol, close, pct_b
FROM (SELECT time, symbol, close,
             round(bb_percent_b(close) OVER (PARTITION BY symbol ORDER BY time), 3) AS pct_b
      FROM ochl_minutes) t
WHERE time = '2026-09-10T09:14:00Z'
ORDER BY symbol;

Result:

symbol (SYMBOL)close (DOUBLE)pct_b (DOUBLE)
CITY504.90.46
COAST376.10.901

(2 rows)

bb_width

Syntax:

bb_width(x)
bb_width(x, n)
bb_width(x, n, mult)
bb_width(x, n, mult) OVER ([PARTITION BY ...] [ORDER BY ...])

The Bollinger band width: (upper band - lower band) / middle band. It is how far apart the bands are, as a part of the price. 0.02 means the bands are 2% of the price apart. Very small widths are a squeeze: the market is quiet, and traders wait for a big move to start.

ArgumentTypeMeaning
xnumberthe price, usually close
nwhole number, 1 or morehow many candles; default 20
multnumberhow many standard deviations; default 2

Example: The quietest minutes of COAST: the smallest band width (sample data: sample)

SELECT time, close, round(bb_width(close) * 100, 3) AS width_pct
FROM ochl_minutes
WHERE symbol = 'COAST'
ORDER BY width_pct
LIMIT 3;

Result:

time (TIMESTAMP)close (DOUBLE)width_pct (DOUBLE)
2026-09-10T08:04:00.000Z3780.229
2026-09-10T08:03:00.000Z377.80.24
2026-09-10T06:01:00.000Z380.30.246

(3 rows)

Example: Daily 5-day band width of every symbol (sample data: sample)

SELECT time, symbol, close,
       round(bb_width(close, 5) OVER (PARTITION BY symbol ORDER BY time), 4) AS width
FROM ochl_day
WHERE time >= '2026-09-01'
ORDER BY time DESC, symbol
LIMIT 9;

Result:

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)width (DOUBLE)
2026-09-10T00:00:00.000ZCITY504.90.0252
2026-09-10T00:00:00.000ZCOAST376.10.019
2026-09-10T00:00:00.000ZVOLT191.20.1111
2026-09-09T00:00:00.000ZCITY504.60.0207
2026-09-09T00:00:00.000ZCOAST380.10.0266
2026-09-09T00:00:00.000ZVOLT187.30.0904
2026-09-08T00:00:00.000ZCITY497.50.0074
2026-09-08T00:00:00.000ZCOAST377.20.0368
2026-09-08T00:00:00.000ZVOLT182.40.084

(9 rows)

kc_upper, kc_lower

Syntax:

kc_upper(high, low, close)
kc_upper(high, low, close, n)
kc_upper(high, low, close, n, mult)
kc_upper(high, low, close, n, mult, atr_n)
kc_lower(high, low, close, n, mult, atr_n)
kc_upper(high, low, close, n, mult, atr_n) OVER ([PARTITION BY ...] [ORDER BY ...])

The Keltner channel: a middle line, the ema of the close, and two lines mult × atr above and below it. It looks like the Bollinger bands, but the width comes from the ATR, so it changes more smoothly. A close above the upper line shows a strong up trend; a close below the lower line a strong down trend.

ArgumentTypeMeaning
highnumberthe candle's high
lownumberthe candle's low
closenumberthe candle's close
nwhole number, 1 or morethe EMA period; default 20
multnumberhow many ATRs away; default 2
atr_nwhole number, 1 or morethe ATR period; default 10

Example: Keltner channel of CITY's one-minute candles (the last 5 minutes) (sample data: sample)

SELECT time, close, kc_low, kc_mid, kc_high
FROM (SELECT time, close,
             round(kc_lower(high, low, close), 2) AS kc_low,
             round(kc_middle(high, low, close), 2) AS kc_mid,
             round(kc_upper(high, low, close), 2) AS kc_high
      FROM ochl_minutes
      WHERE symbol = 'CITY') t
ORDER BY time DESC
LIMIT 5;

Result:

time (TIMESTAMP)close (DOUBLE)kc_low (DOUBLE)kc_mid (DOUBLE)kc_high (DOUBLE)
2026-09-10T09:14:00.000Z504.9503.83505.06506.28
2026-09-10T09:13:00.000Z505.1503.87505.07506.27
2026-09-10T09:12:00.000Z504.8503.87505.07506.27
2026-09-10T09:11:00.000Z504.5503.87505.1506.32
2026-09-10T09:10:00.000Z504.6503.82505.16506.5

(5 rows)

Example: The squeeze: Bollinger bands inside the Keltner channel (sample data: sample)

SELECT symbol, count(*) AS minutes, count_if(bb_high < kc_high AND bb_low > kc_low) AS squeeze_minutes
FROM (SELECT symbol,
             bb_upper(close) OVER (PARTITION BY symbol ORDER BY time) AS bb_high,
             bb_lower(close) OVER (PARTITION BY symbol ORDER BY time) AS bb_low,
             kc_upper(high, low, close) OVER (PARTITION BY symbol ORDER BY time) AS kc_high,
             kc_lower(high, low, close) OVER (PARTITION BY symbol ORDER BY time) AS kc_low
      FROM ochl_minutes) t
GROUP BY symbol
ORDER BY symbol;

Result:

symbol (SYMBOL)minutes (LONG)squeeze_minutes (LONG)
CITY480242
COAST476171

(2 rows)

Example: The first value: candle 5 (n = 3, atr_n = 4) (sample data: sample)

SELECT time, close,
       round(kc_upper(high, low, close, 3, 2, 4), 2) AS kc_high,
       round(kc_lower(high, low, close, 3, 2, 4), 2) AS kc_low
FROM ochl_day
WHERE symbol = 'COAST' AND time >= '2026-08-23'
ORDER BY time
LIMIT 6;

Result:

time (TIMESTAMP)close (DOUBLE)kc_high (DOUBLE)kc_low (DOUBLE)
2026-08-23T00:00:00.000Z391NULLNULL
2026-08-24T00:00:00.000Z384.9NULLNULL
2026-08-25T00:00:00.000Z379.8NULLNULL
2026-08-26T00:00:00.000Z373NULLNULL
2026-08-27T00:00:00.000Z367.1389.81356.41
2026-08-30T00:00:00.000Z372.4390.88354.63

(6 rows)

kc_middle

Syntax:

kc_middle(high, low, close)
kc_middle(high, low, close, n)
kc_middle(high, low, close, n) OVER ([PARTITION BY ...] [ORDER BY ...])

The middle line of the Keltner channel: the n-candle ema of the close.

ArgumentTypeMeaning
highnumberthe candle's high (read, but not used in the value)
lownumberthe candle's low (read, but not used in the value)
closenumberthe candle's close
nwhole number, 1 or morethe EMA period; default 20

Example: kc_middle is the EMA of the close (sample data: sample)

SELECT time, close,
       round(kc_middle(high, low, close, 5), 3) AS kc_mid,
       round(ema(close, 5), 3) AS ema5
FROM ochl_day
WHERE symbol = 'CITY' AND time >= '2026-08-23'
ORDER BY time
LIMIT 7;

Result:

time (TIMESTAMP)close (DOUBLE)kc_mid (DOUBLE)ema5 (DOUBLE)
2026-08-23T00:00:00.000Z507.9NULLNULL
2026-08-24T00:00:00.000Z510.3NULLNULL
2026-08-25T00:00:00.000Z513.3NULLNULL
2026-08-26T00:00:00.000Z513.2NULLNULL
2026-08-27T00:00:00.000Z505.9510.12510.12
2026-08-30T00:00:00.000Z503.1507.78507.78
2026-08-31T00:00:00.000Z505.7507.087507.087

(7 rows)

dc_upper, dc_middle, dc_lower

Syntax:

dc_upper(high, low)    dc_upper(high, low, n)
dc_middle(high, low)   dc_middle(high, low, n)
dc_lower(high, low)    dc_lower(high, low, n)
dc_upper(high, low, n) OVER ([PARTITION BY ...] [ORDER BY ...])

The Donchian channel: dc_upper is the highest high of the last n candles, dc_lower the lowest low, and dc_middle is halfway between them. This candle is one of the n.

When the price goes above the highest high of the last 20 candles, it is a breakout. The famous "turtle traders" bought such breakouts and sold when the price fell below the lowest low of the last 10 candles.

ArgumentTypeMeaning
highnumberthe candle's high
lownumberthe candle's low
nwhole number, 1 or morehow many candles; default 20

Example: 5-day channel of VOLT, and max(high) over the same rows (sample data: sample)

SELECT time, high, low,
       dc_lower(high, low, 5) AS low_5,
       dc_middle(high, low, 5) AS middle_5,
       dc_upper(high, low, 5) AS high_5,
       max(high) OVER (ORDER BY time ROWS 4 PRECEDING) AS max_high_5
FROM ochl_day
WHERE symbol = 'VOLT' AND time >= '2026-08-23'
ORDER BY time
LIMIT 7;

Result:

time (TIMESTAMP)high (DOUBLE)low (DOUBLE)low_5 (DOUBLE)middle_5 (DOUBLE)high_5 (DOUBLE)max_high_5 (DOUBLE)
2026-08-23T00:00:00.000Z182.6178.2NULLNULLNULL182.6
2026-08-24T00:00:00.000Z183.6178.5NULLNULLNULL183.6
2026-08-25T00:00:00.000Z182.2172.8NULLNULLNULL183.6
2026-08-26T00:00:00.000Z178.4172NULLNULLNULL183.6
2026-08-27T00:00:00.000Z175.9173172177.8183.6183.6
2026-08-30T00:00:00.000Z174.2170.6170.6177.1183.6183.6
2026-08-31T00:00:00.000Z175.6171.3170.6176.4182.2182.2

(7 rows)

Example: Turtle breakout: the close is above the high of the previous 20 minutes (sample data: sample)

SELECT time, symbol, close, prev_high_20
FROM (SELECT time, symbol, close,
             lag(high_20) OVER (PARTITION BY symbol ORDER BY time) AS prev_high_20
      FROM (SELECT time, symbol, close,
                   dc_upper(high, low, 20) OVER (PARTITION BY symbol ORDER BY time) AS high_20
            FROM ochl_minutes
            WHERE time IN '2026-09-10') a) b
WHERE close > prev_high_20
ORDER BY time
LIMIT 6;

Result:

time (TIMESTAMP)symbol (SYMBOL)close (DOUBLE)prev_high_20 (DOUBLE)
2026-09-10T05:42:00.000ZCITY505.7505.6
2026-09-10T05:43:00.000ZCITY506505.7
2026-09-10T05:44:00.000ZCITY506.5506.2
2026-09-10T06:03:00.000ZCOAST380.9380.7
2026-09-10T06:04:00.000ZCOAST381380.9
2026-09-10T06:15:00.000ZCOAST381.5381.4

(6 rows)

Example: The period must be 1 or more (sample data: sample; shows an error)

SELECT dc_upper(high, low, 0) FROM ochl_day;

Result:

Error: line 1, column 8: dc_upper: the period must be a whole number of rows (1 or more)