# Volatility indicators and bands > How much the price moves: true range, ATR in percent, Bollinger bands, Keltner and Donchian channels. - Category: Functions — technical indicators - Keywords: volatility, bands, channel, envelope, breakout, squeeze, true range, TR, TRANGE, normalized ATR, NATR, ATR percent, Bollinger bands, Bollinger, BBANDS, BB, upper band, lower band, middle band, %B, percent b, bandwidth, band width, Bollinger squeeze, Keltner channel, Keltner, KC, Donchian channel, Donchian, price channel, turtle, 20-day high, stop loss, position size, stock market - See also: [Technical indicators](functions-indicators.md), [Momentum indicators](functions-indicators-momentum.md), [Trend indicators](functions-indicators-trend.md), [Volume indicators](functions-indicators-volume.md), [Returns and risk (volatility)](functions-returns-risk.md#volatility), [Window and series functions](functions-window.md) **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: - put a **stop-loss** (a price where you sell to stop a bigger loss) far enough away that normal moves do not hit it; - draw **bands** (or channels) around the price. A band is a line above and a line below the price. When the price leaves a band, something unusual happens: a **breakout** (a strong move out of the normal range). These functions are **series functions**, like [sma and atr](functions-indicators.md). The same rules apply: - **Order:** without `OVER`, all result rows are one series in time order. With **many symbols**, always write `OVER (PARTITION BY symbol ORDER BY time)`. - **Period:** `n` is how many candles the indicator looks back, a whole number of 1 or more. - **WHERE runs first**, and candles made with `SAMPLE BY` must be made in a subquery: see the rules in [technical indicators](functions-indicators.md). - **Warm-up:** at the start of a series there are not enough candles yet, and the result is `NULL`: | Function (default periods) | First value on candle | NULL candles at the start | |---|---|---| | `trange(high, low, close)` | 2 | 1 | | `natr(high, low, close, n)` (n = 14) | n + 1 | n | | `bb_upper`, `bb_middle`, `bb_lower`, `bb_percent_b`, `bb_width` `(x, n)` (n = 20) | n | n - 1 | | `kc_middle(high, low, close, n)` (n = 20) | n | n - 1 | | `kc_upper`, `kc_lower` `(high, low, close, n, mult, atr_n)` (20, 2, 10) | the bigger of n and atr_n + 1 | one less | | `dc_upper`, `dc_middle`, `dc_lower` `(high, low, n)` (n = 20) | n | n - 1 | - **NULL prices:** `trange`, the Bollinger bands and the Donchian channel look at a window of candles: a NULL makes the windows that hold it `NULL`, and then the values come back. `natr` and the Keltner channel are built step by step (like `ema` and `atr`): one NULL makes this and **every later** value `NULL`. Leave NULL candles out with `WHERE`. - **Same as TA-Lib:** `trange`, `natr` and the Bollinger bands give the same numbers as TA-Lib (TRANGE, NATR, BBANDS with a simple moving average). - **Returns** `DOUBLE`. Use `round(x, 2)` for prices. **Example: Bollinger bands, ATR in percent and the 20-minute channel of CITY (the last 5 minutes)** (sample data: sample) ```sql 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.000Z | 504.9 | 504.1 | 504.97 | 505.84 | 0.122 | 503.9 | 506.1 | | 2026-09-10T09:13:00.000Z | 505.1 | 504.04 | 504.95 | 505.85 | 0.12 | 503.9 | 506.1 | | 2026-09-10T09:12:00.000Z | 504.8 | 504.04 | 504.94 | 505.84 | 0.121 | 503.9 | 506.1 | | 2026-09-10T09:11:00.000Z | 504.5 | 504.03 | 504.94 | 505.84 | 0.122 | 503.9 | 506.1 | | 2026-09-10T09:10:00.000Z | 504.6 | 504.08 | 504.98 | 505.87 | 0.13 | 503.9 | 506.1 | (5 rows) ## trange, true_range Syntax: ```sql 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](functions-indicators.md#atr) is the average of the true range. | Argument | Type | Meaning | |---|---|---| | `high` | number | the candle's high | | `low` | number | the candle's low | | `close` | number | the candle's close (the previous candle's close is used) | - **Returns:** `DOUBLE`, in the same unit as the prices. - **NULL:** `NULL` for the first candle (it has no previous close). A NULL `high` or `low` makes only that candle `NULL`. When the previous close is NULL, only `high - low` is used. - **Notes:** there is no period. The same as TA-Lib TRANGE. The result can show rounding digits (`9.80000000000001`): use `round(x, 2)`. **Example: A gap counts: COAST on 9 September** (sample data: sample) ```sql 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.000Z | 373.6 | 364.1 | NULL | 9.5 | NULL | | 2026-09-06T00:00:00.000Z | 378.5 | 372.5 | 373.2 | 6 | 6 | | 2026-09-07T00:00:00.000Z | 381.1 | 373.2 | 375.4 | 7.9 | 7.9 | | 2026-09-08T00:00:00.000Z | 380.2 | 374.7 | 379.2 | 5.5 | 5.5 | | 2026-09-09T00:00:00.000Z | 398.8 | 379.2 | 377.2 | 19.6 | 21.6 | | 2026-09-10T00:00:00.000Z | 382 | 373.2 | 380.1 | 8.8 | 8.8 | (6 rows) **Example: Plain 5-day average of the true range against Wilder's ATR** (sample data: sample) ```sql 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.000Z | NULL | NULL | NULL | | 2026-08-24T00:00:00.000Z | 5.1 | 5.1 | NULL | | 2026-08-25T00:00:00.000Z | 9.4 | 7.25 | NULL | | 2026-08-26T00:00:00.000Z | 6.4 | 6.97 | NULL | | 2026-08-27T00:00:00.000Z | 2.9 | 5.95 | NULL | | 2026-08-30T00:00:00.000Z | 3.6 | 5.48 | 5.48 | | 2026-08-31T00:00:00.000Z | 4.3 | 5.32 | 5.24 | | 2026-09-01T00:00:00.000Z | 5.3 | 4.5 | 5.26 | | 2026-09-02T00:00:00.000Z | 5.6 | 4.34 | 5.32 | | 2026-09-03T00:00:00.000Z | 5.8 | 4.92 | 5.42 | | 2026-09-06T00:00:00.000Z | 11.5 | 6.5 | 6.64 | | 2026-09-07T00:00:00.000Z | 8.9 | 7.42 | 7.09 | | 2026-09-08T00:00:00.000Z | 6.6 | 7.68 | 6.99 | | 2026-09-09T00:00:00.000Z | 7.8 | 8.12 | 7.15 | | 2026-09-10T00:00:00.000Z | 7.9 | 8.54 | 7.3 | (15 rows) **Example: high, low and close are all needed** (sample data: sample; shows an error) ```sql SELECT trange(high, low) FROM ochl_day; ``` Result: Error: `line 1, column 8: function 'trange' takes 3 argument(s), got 2` ## natr Syntax: ```sql natr(high, low, close) natr(high, low, close, n) natr(high, low, close, n) OVER ([PARTITION BY ...] [ORDER BY ...]) ``` The **normalized ATR**: the [atr](functions-indicators.md#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. | Argument | Type | Meaning | |---|---|---| | `high` | number | the candle's high | | `low` | number | the candle's low | | `close` | number | the candle's close | | `n` | whole number, 1 or more | the ATR period; default `14` | - **Returns:** `DOUBLE`, a percent. - **NULL:** `NULL` for the first `n` candles. A close of `0` gives `NULL`. **Careful:** a NULL `high`, `low` or `close` makes this and every later value of the series `NULL`. - **Notes:** `natr(high, low, close, n)` is `atr(high, low, close, n) / close * 100` of the same candle. **Example: Which share moves most? 5-day NATR on the last day** (sample data: sample) ```sql 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) | |---|---|---| | VOLT | 191.2 | 3.82 | | COAST | 376.1 | 2.58 | | CITY | 504.9 | 1.45 | (3 rows) **Example: A stop-loss 2 × NATR percent below the close is the same as 2 × ATR** (sample data: sample) ```sql 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) | |---|---|---|---|---| | CITY | 504.9 | 1.45 | 490.2 | 490.2 | | COAST | 376.1 | 2.58 | 356.7 | 356.7 | | VOLT | 191.2 | 3.82 | 176.6 | 176.6 | (3 rows) **Example: NATR 14 of COAST's one-minute candles (the last 5 minutes)** (sample data: sample) ```sql 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.000Z | 376.1 | 0.1513 | | 2026-09-10T09:13:00.000Z | 376.2 | 0.1526 | | 2026-09-10T09:12:00.000Z | 376 | 0.1522 | | 2026-09-10T09:11:00.000Z | 375.5 | 0.1477 | | 2026-09-10T09:10:00.000Z | 375.6 | 0.1508 | (5 rows) ## bb_upper, bb_middle, bb_lower Syntax: ```sql 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](functions-indicators.md#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. | Argument | Type | Meaning | |---|---|---| | `x` | number | the price, usually `close` | | `n` | whole number, 1 or more | how many candles; default `20` | | `mult` | number | how many standard deviations away; default `2` (`2.5` is allowed) | - **Returns:** `DOUBLE`. - **NULL:** `NULL` for the first `n - 1` candles. A NULL value makes the `n` results whose window holds it `NULL`; after that the bands come back. - **Notes:** - The standard deviation divides by `n` (population), like John Bollinger and TA-Lib BBANDS. - `bb_middle` is the same as `sma(x, n)`. It takes `mult` too, so you can copy the same arguments to all three. - With a small `n` the close cannot leave the bands: with `n` of 5 or less and `mult` 2, it is always inside. Use the usual `n` of 20, or a smaller `mult`. **Example: 4-day bands of CITY with mult 1.5: is the close inside?** (sample data: sample) ```sql 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.000Z | 507.9 | NULL | NULL | NULL | NULL | | 2026-08-24T00:00:00.000Z | 510.3 | NULL | NULL | NULL | NULL | | 2026-08-25T00:00:00.000Z | 513.3 | NULL | NULL | NULL | NULL | | 2026-08-26T00:00:00.000Z | 513.2 | 507.81 | 511.18 | 514.54 | inside | | 2026-08-27T00:00:00.000Z | 505.9 | 506.16 | 510.68 | 515.19 | below | | 2026-08-30T00:00:00.000Z | 503.1 | 502.15 | 508.88 | 515.6 | inside | | 2026-08-31T00:00:00.000Z | 505.7 | 501.34 | 506.98 | 512.61 | inside | | 2026-09-01T00:00:00.000Z | 502.4 | 501.96 | 504.28 | 506.59 | inside | | 2026-09-02T00:00:00.000Z | 496.4 | 496.79 | 501.9 | 507.01 | below | | 2026-09-03T00:00:00.000Z | 497.9 | 495.08 | 500.6 | 506.12 | inside | | 2026-09-06T00:00:00.000Z | 499 | 495.61 | 498.93 | 502.24 | inside | | 2026-09-07T00:00:00.000Z | 498.7 | 496.49 | 498 | 499.51 | inside | | 2026-09-08T00:00:00.000Z | 497.5 | 497.37 | 498.28 | 499.18 | inside | | 2026-09-09T00:00:00.000Z | 504.6 | 495.84 | 499.95 | 504.06 | above | | 2026-09-10T00:00:00.000Z | 504.9 | 496.39 | 501.43 | 506.46 | inside | (15 rows) **Example: Breakouts: the minutes where the close crosses above the upper band (20, 2)** (sample data: sample) ```sql 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.000Z | CITY | 505.7 | 505.579 | | 2026-09-10T06:03:00.000Z | COAST | 380.9 | 380.614 | | 2026-09-10T06:24:00.000Z | CITY | 506.4 | 506.264 | | 2026-09-10T07:26:00.000Z | CITY | 503.1 | 503.087 | | 2026-09-10T07:35:00.000Z | COAST | 378.8 | 378.617 | | 2026-09-10T07:41:00.000Z | CITY | 503.6 | 503.6 | | 2026-09-10T07:43:00.000Z | CITY | 503.7 | 503.7 | | 2026-09-10T08:21:00.000Z | CITY | 504.2 | 504.179 | | 2026-09-10T08:28:00.000Z | COAST | 378 | 377.938 | | 2026-09-10T08:39:00.000Z | CITY | 506.3 | 506.298 | | 2026-09-10T08:42:00.000Z | CITY | 506.9 | 506.823 | (11 rows) **Example: Wider bands with mult 3** (sample data: sample) ```sql 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.000Z | 176 | NULL | NULL | NULL | NULL | | 2026-09-06T00:00:00.000Z | 180.1 | NULL | NULL | NULL | NULL | | 2026-09-07T00:00:00.000Z | 176.9 | NULL | NULL | NULL | NULL | | 2026-09-08T00:00:00.000Z | 182.4 | NULL | NULL | NULL | NULL | | 2026-09-09T00:00:00.000Z | 187.3 | 172.38 | 168.3 | 188.7 | 192.78 | | 2026-09-10T00:00:00.000Z | 191.2 | 173.38 | 168.28 | 193.78 | 198.88 | (6 rows) **Example: mult must be a number** (sample data: sample; shows an error) ```sql 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: ```sql 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)`. | %B | The price is | |---|---| | more than `1` | above the upper band | | `1` | on the upper band | | `0.5` | on the middle band | | `0` | on the lower band | | less than `0` | below the lower band | | Argument | Type | Meaning | |---|---|---| | `x` | number | the price, usually `close` | | `n` | whole number, 1 or more | how many candles; default `20` | | `mult` | number | how many standard deviations; default `2` | - **Returns:** `DOUBLE`. - **NULL:** `NULL` for the first `n - 1` candles, and when the bands are equal (all `n` prices are the same). A NULL value makes the windows that hold it `NULL`. - **Notes:** one number instead of three lines: easy to use in `WHERE` or `CASE`. **Example: Where is VOLT inside its 5-day bands?** (sample data: sample) ```sql 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.000Z | 181 | NULL | lower half | | 2026-08-24T00:00:00.000Z | 178.5 | NULL | lower half | | 2026-08-25T00:00:00.000Z | 176.8 | NULL | lower half | | 2026-08-26T00:00:00.000Z | 174.2 | NULL | lower half | | 2026-08-27T00:00:00.000Z | 173.3 | 0.192 | lower half | | 2026-08-30T00:00:00.000Z | 173 | 0.248 | lower half | | 2026-08-31T00:00:00.000Z | 172.6 | 0.271 | lower half | | 2026-09-01T00:00:00.000Z | 169 | 0.022 | lower half | | 2026-09-02T00:00:00.000Z | 171.5 | 0.439 | lower half | | 2026-09-03T00:00:00.000Z | 176 | 0.895 | upper half | | 2026-09-06T00:00:00.000Z | 180.1 | 0.906 | upper half | | 2026-09-07T00:00:00.000Z | 176.9 | 0.639 | upper half | | 2026-09-08T00:00:00.000Z | 182.4 | 0.837 | upper half | | 2026-09-09T00:00:00.000Z | 187.3 | 0.914 | upper half | | 2026-09-10T00:00:00.000Z | 191.2 | 0.874 | upper half | (15 rows) **Example: %B of both symbols at the last minute (20, 2)** (sample data: sample) ```sql 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) | |---|---|---| | CITY | 504.9 | 0.46 | | COAST | 376.1 | 0.901 | (2 rows) ## bb_width Syntax: ```sql 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. | Argument | Type | Meaning | |---|---|---| | `x` | number | the price, usually `close` | | `n` | whole number, 1 or more | how many candles; default `20` | | `mult` | number | how many standard deviations; default `2` | - **Returns:** `DOUBLE`. - **NULL:** `NULL` for the first `n - 1` candles, and when the middle band is `0`. A NULL value makes the windows that hold it `NULL`. - **Notes:** a part, not a percent: multiply by 100 for percent. **Example: The quietest minutes of COAST: the smallest band width** (sample data: sample) ```sql 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.000Z | 378 | 0.229 | | 2026-09-10T08:03:00.000Z | 377.8 | 0.24 | | 2026-09-10T06:01:00.000Z | 380.3 | 0.246 | (3 rows) **Example: Daily 5-day band width of every symbol** (sample data: sample) ```sql 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.000Z | CITY | 504.9 | 0.0252 | | 2026-09-10T00:00:00.000Z | COAST | 376.1 | 0.019 | | 2026-09-10T00:00:00.000Z | VOLT | 191.2 | 0.1111 | | 2026-09-09T00:00:00.000Z | CITY | 504.6 | 0.0207 | | 2026-09-09T00:00:00.000Z | COAST | 380.1 | 0.0266 | | 2026-09-09T00:00:00.000Z | VOLT | 187.3 | 0.0904 | | 2026-09-08T00:00:00.000Z | CITY | 497.5 | 0.0074 | | 2026-09-08T00:00:00.000Z | COAST | 377.2 | 0.0368 | | 2026-09-08T00:00:00.000Z | VOLT | 182.4 | 0.084 | (9 rows) ## kc_upper, kc_lower Syntax: ```sql 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](functions-indicators.md#ema) of the close, and two lines `mult` × [atr](functions-indicators.md#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. | Argument | Type | Meaning | |---|---|---| | `high` | number | the candle's high | | `low` | number | the candle's low | | `close` | number | the candle's close | | `n` | whole number, 1 or more | the EMA period; default `20` | | `mult` | number | how many ATRs away; default `2` | | `atr_n` | whole number, 1 or more | the ATR period; default `10` | - **Returns:** `DOUBLE`. - **NULL:** the first value is on candle `n` or candle `atr_n + 1`, whichever comes later (candle 20 with the defaults). **Careful:** a NULL `high`, `low` or `close` makes this and every later value of the series `NULL`. - **Notes:** the middle line is [kc_middle](functions-indicators-volatility.md#kc_middle). The EMA starts with the SMA of the first `n` closes and the ATR is Wilder's, like `ema` and `atr`. **Example: Keltner channel of CITY's one-minute candles (the last 5 minutes)** (sample data: sample) ```sql 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.000Z | 504.9 | 503.83 | 505.06 | 506.28 | | 2026-09-10T09:13:00.000Z | 505.1 | 503.87 | 505.07 | 506.27 | | 2026-09-10T09:12:00.000Z | 504.8 | 503.87 | 505.07 | 506.27 | | 2026-09-10T09:11:00.000Z | 504.5 | 503.87 | 505.1 | 506.32 | | 2026-09-10T09:10:00.000Z | 504.6 | 503.82 | 505.16 | 506.5 | (5 rows) **Example: The squeeze: Bollinger bands inside the Keltner channel** (sample data: sample) ```sql 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) | |---|---|---| | CITY | 480 | 242 | | COAST | 476 | 171 | (2 rows) **Example: The first value: candle 5 (n = 3, atr_n = 4)** (sample data: sample) ```sql 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.000Z | 391 | NULL | NULL | | 2026-08-24T00:00:00.000Z | 384.9 | NULL | NULL | | 2026-08-25T00:00:00.000Z | 379.8 | NULL | NULL | | 2026-08-26T00:00:00.000Z | 373 | NULL | NULL | | 2026-08-27T00:00:00.000Z | 367.1 | 389.81 | 356.41 | | 2026-08-30T00:00:00.000Z | 372.4 | 390.88 | 354.63 | (6 rows) ## kc_middle Syntax: ```sql 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](functions-indicators-volatility.md#kc_upper): the `n`-candle [ema](functions-indicators.md#ema) of the close. | Argument | Type | Meaning | |---|---|---| | `high` | number | the candle's high (read, but not used in the value) | | `low` | number | the candle's low (read, but not used in the value) | | `close` | number | the candle's close | | `n` | whole number, 1 or more | the EMA period; default `20` | - **Returns:** `DOUBLE`. - **NULL:** `NULL` for the first `n - 1` candles. **Careful:** a NULL `high`, `low` or `close` makes this and every later value `NULL`. - **Notes:** the same as `ema(close, n)` when no price is NULL. It takes `high` and `low` so the three Keltner functions can have the same first arguments. **Example: kc_middle is the EMA of the close** (sample data: sample) ```sql 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.000Z | 507.9 | NULL | NULL | | 2026-08-24T00:00:00.000Z | 510.3 | NULL | NULL | | 2026-08-25T00:00:00.000Z | 513.3 | NULL | NULL | | 2026-08-26T00:00:00.000Z | 513.2 | NULL | NULL | | 2026-08-27T00:00:00.000Z | 505.9 | 510.12 | 510.12 | | 2026-08-30T00:00:00.000Z | 503.1 | 507.78 | 507.78 | | 2026-08-31T00:00:00.000Z | 505.7 | 507.087 | 507.087 | (7 rows) ## dc_upper, dc_middle, dc_lower Syntax: ```sql 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. | Argument | Type | Meaning | |---|---|---| | `high` | number | the candle's high | | `low` | number | the candle's low | | `n` | whole number, 1 or more | how many candles; default `20` | - **Returns:** `DOUBLE`. - **NULL:** `NULL` for the first `n - 1` candles. A NULL `high` or `low` makes the `n` results whose window holds it `NULL`; after that the channel comes back. - **Notes:** - `dc_upper(high, low, n)` is like `max(high) OVER (... ROWS n-1 PRECEDING)`, but it waits until it has `n` candles. - Because this candle is inside the window, the close can never be above `dc_upper` of the same candle. To find a breakout, compare the close with the **previous** candle's value: `lag(...)` in an outer query. **Example: 5-day channel of VOLT, and max(high) over the same rows** (sample data: sample) ```sql 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.000Z | 182.6 | 178.2 | NULL | NULL | NULL | 182.6 | | 2026-08-24T00:00:00.000Z | 183.6 | 178.5 | NULL | NULL | NULL | 183.6 | | 2026-08-25T00:00:00.000Z | 182.2 | 172.8 | NULL | NULL | NULL | 183.6 | | 2026-08-26T00:00:00.000Z | 178.4 | 172 | NULL | NULL | NULL | 183.6 | | 2026-08-27T00:00:00.000Z | 175.9 | 173 | 172 | 177.8 | 183.6 | 183.6 | | 2026-08-30T00:00:00.000Z | 174.2 | 170.6 | 170.6 | 177.1 | 183.6 | 183.6 | | 2026-08-31T00:00:00.000Z | 175.6 | 171.3 | 170.6 | 176.4 | 182.2 | 182.2 | (7 rows) **Example: Turtle breakout: the close is above the high of the previous 20 minutes** (sample data: sample) ```sql 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.000Z | CITY | 505.7 | 505.6 | | 2026-09-10T05:43:00.000Z | CITY | 506 | 505.7 | | 2026-09-10T05:44:00.000Z | CITY | 506.5 | 506.2 | | 2026-09-10T06:03:00.000Z | COAST | 380.9 | 380.7 | | 2026-09-10T06:04:00.000Z | COAST | 381 | 380.9 | | 2026-09-10T06:15:00.000Z | COAST | 381.5 | 381.4 | (6 rows) **Example: The period must be 1 or more** (sample data: sample; shows an error) ```sql 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)`