--- title: "Candlestick Pattern Recognition" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Candlestick Pattern Recognition} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, message = FALSE, warning = FALSE, comment = "#>", out.width = "100%", out.height = "520", fig.align = "center" ) ``` [{talib}](https://github.com/serkor1/ta-lib-R/) includes 61 candlestick pattern recognition functions ported from [TA-Lib](https://github.com/TA-Lib/ta-lib). This vignette covers how they work, what they return, and how to tune their sensitivity. ## A primer on candlesticks A candlestick shows four prices for a chosen time period: Open, High, Low, and Close (OHLC). ```{r, echo = FALSE} # two example candles df <- data.frame( date = as.Date(c("2021-01-01", "2021-01-02")), open = c(30000, 31500), high = c(32000, 31800), low = c(29500, 30500), close = c(31500, 30800) ) mid <- (df$close + df$open) / 2 annos <- list( list( x = df$date[1], y = mid[1], text = "Candle body (Bullish)", showarrow = FALSE, ax = 0, ay = -40, arrowhead = 2, arrowcolor = "#65a479", font = list(size = 12) ), list( x = df$date[1], y = df$high[1], text = "Upper shadow", showarrow = TRUE, ax = 0, ay = -40, arrowhead = 2, arrowcolor = "gray40", font = list(size = 12) ), list( x = df$date[1], y = df$low[1], text = "Lower shadow", showarrow = TRUE, ax = 0, ay = 40, arrowhead = 2, arrowcolor = "gray40", font = list(size = 12) ), list( x = df$date[2], y = mid[2], text = "Candle body (Bearish)", showarrow = FALSE, ax = 0, ay = 40, arrowhead = 2, arrowcolor = "#d5695d", font = list(size = 12) ), list( x = df$date[2], y = df$high[2], text = "Upper shadow", showarrow = TRUE, ax = 0, ay = -40, arrowhead = 2, arrowcolor = "gray40", font = list(size = 12) ), list( x = df$date[2], y = df$low[2], text = "Lower shadow", showarrow = TRUE, ax = 0, ay = 40, arrowhead = 2, arrowcolor = "gray40", font = list(size = 12) ) ) df |> plotly::plot_ly( x = ~date, open = ~open, high = ~high, low = ~low, close = ~close, type = "candlestick", increasing = list( line = list(color = "#65a479"), fillcolor = "#65a479" ), decreasing = list( line = list(color = "#d5695d"), fillcolor = "#d5695d" ) ) |> plotly::layout( title = "", xaxis = list( title = "", rangeslider = list(visible = FALSE), showticklabels = FALSE, showgrid = FALSE), yaxis = list( title = "", showticklabels = FALSE, showgrid = FALSE), annotations = annos ) ``` The **real body** is the segment between the Open and the Close. If the Close is above the Open the candle is bullish; otherwise it is bearish. The **upper shadow** extends from the top of the real body to the High; the **lower shadow** extends from the bottom of the real body to the Low. Together the shadows show how far price moved beyond the Open-Close range. Candlestick patterns are defined by comparing the size and position of bodies and shadows---within a single candle or across a sequence of consecutive candles. ## Available patterns The 61 pattern functions can be loosely grouped by how many candles they inspect: | Candles | Examples | |:--------|:---------| | Single-candle | `doji()`, `hammer()`, `shooting_star()`, `marubozu()`, `spinning_top()`, `long_line()`, `short_line()` | | Two-candle | `engulfing()`, `harami()`, `harami_cross()`, `piercing()`, `dark_cloud_cover()`, `kicking()` | | Three-candle | `morning_star()`, `evening_star()`, `three_inside()`, `three_outside()`, `three_white_soldiers()`, `three_black_crows()` | | Four+ candle | `concealing_baby_swallow()`, `rising_falling_three_methods()`, `mat_hold()`, `break_away()` | Every function has a descriptive snake_case name and an uppercase alias matching the TA-Lib convention (`doji` / `CDLDOJI`, `engulfing` / `CDLENGULFING`, etc.). ## Return values All pattern functions require OHLC columns (`~open + high + low + close`) and return an integer matrix of the same length as the input: - **`1`** --- bullish pattern (or direction-neutral pattern) identified - **`-1`** --- bearish pattern identified - **`0`** --- no pattern ```{r} x <- talib::doji(talib::BTC) table(x) ``` Some patterns are inherently directional (e.g., `engulfing()` returns both `1` and `-1`), while others are direction-neutral (e.g., `doji()` always returns `1` for identified patterns). ### Output encoding By default, patterns are normalized to `1`/`-1`/`0`. The original TA-Lib encoding (`100`/`-100`/`0`) can be restored with: ```{r} options(talib.normalize = FALSE) table(talib::engulfing(talib::BTC)) ``` ```{r, include = FALSE} ## restore default options(talib.normalize = TRUE) ``` ### The `eps` parameter Seven patterns accept an `eps` (penetration) parameter that controls how far one candle must intrude into the body of another. These are `morning_star()`, `evening_star()`, `morning_doji_star()`, `evening_doji_star()`, `abandoned_baby()`, `dark_cloud_cover()`, and `mat_hold()`. The default is `eps = 0` for all of them. ```{r} ## Evening Star with 30% penetration x <- talib::evening_star(talib::BTC, eps = 0.3) sum(abs(x), na.rm = TRUE) ``` ## Lookback and `NA` values Every pattern has a **lookback period**: the number of initial rows that are returned as `NA` because the algorithm needs historical context before it can evaluate. The lookback depends on two things: the number of candles in the pattern itself, and the `N` parameter that controls how many prior candles are used to compute reference values for body/shadow classification. ```{r} x <- talib::doji(talib::BTC) attr(x, "lookback") ``` ## Tuning candlestick settings Pattern recognition in [{talib}](https://github.com/serkor1/ta-lib-R/) relies on classifying each candle's body and shadows as "long", "short", "doji-like", etc. These classifications are controlled by two parameters per setting: - **`N`** (lookback): How many prior candles to average when computing the reference value. - **`alpha`** (sensitivity): A multiplier applied to the reference value. The current candle is tested against `alpha * reference`. All settings are modified via `options()` and take effect on the next pattern function call. ### Available settings The complete list of settings, their defaults, and what they control: **Body settings:** | Setting | Option prefix | N | alpha | Rule | |:--------|:-------------|--:|------:|:-----| | BodyLong | `talib.BodyLong` | 10 | 1.0 | Body is long when longer than the average of the N previous bodies | | BodyVeryLong | `talib.BodyVeryLong` | 10 | 3.0 | Body is very long when longer than 3x the average | | BodyShort | `talib.BodyShort` | 10 | 1.0 | Body is short when shorter than the average | | BodyDoji | `talib.BodyDoji` | 10 | 0.1 | Body is doji-like when shorter than 10% of the average high-low range | **Shadow settings:** | Setting | Option prefix | N | alpha | Rule | |:--------|:-------------|--:|------:|:-----| | ShadowLong | `talib.ShadowLong` | 0 | 1.0 | Shadow is long when longer than the real body | | ShadowVeryLong | `talib.ShadowVeryLong` | 0 | 2.0 | Shadow is very long when longer than 2x the real body | | ShadowShort | `talib.ShadowShort` | 10 | 1.0 | Shadow is short when shorter than half the average shadow sum | | ShadowVeryShort | `talib.ShadowVeryShort` | 10 | 0.1 | Shadow is very short when shorter than 10% of the average high-low range | **Distance settings:** | Setting | Option prefix | N | alpha | Rule | |:--------|:-------------|--:|------:|:-----| | Near | `talib.Near` | 5 | 0.2 | Distance is "near" when <= 20% of the average high-low range | | Far | `talib.Far` | 5 | 0.6 | Distance is "far" when >= 60% of the average high-low range | | Equal | `talib.Equal` | 5 | 0.05 | Distance is "equal" when <= 5% of the average high-low range | ### Effect of lookback (`N`) Changing `N` alters how many prior candles form the reference average. A shorter lookback makes the reference more reactive to recent price action: ```{r} ## default N = 10 sum(abs(talib::doji(talib::BTC)), na.rm = TRUE) ``` ```{r} ## shorter lookback options(talib.BodyDoji.N = 3) sum(abs(talib::doji(talib::BTC)), na.rm = TRUE) ``` ```{r, include = FALSE} options(talib.BodyDoji.N = 10) ``` ### Effect of sensitivity (`alpha`) Changing `alpha` makes the classification more or less permissive. A higher `alpha` means a wider acceptance threshold: ```{r} ## default alpha = 0.1 sum(abs(talib::doji(talib::BTC)), na.rm = TRUE) ``` ```{r} ## more permissive: accept bodies up to 20% of the high-low range options(talib.BodyDoji.alpha = 0.2) sum(abs(talib::doji(talib::BTC)), na.rm = TRUE) ``` ```{r, include = FALSE} ## restore default options(talib.BodyDoji.alpha = 0.1) ``` The default `alpha = 0.1` for BodyDoji means: "the real body is doji-like when it's shorter than 10% of the average high-low range over the past 10 candles." Doubling it to `0.2` loosens the criterion and identifies more patterns. ### Which settings affect which patterns? Different patterns depend on different settings. As a general rule: - **Doji patterns** (`doji()`, `doji_star()`, `dragonfly_doji()`, `gravestone_doji()`, `long_legged_doji()`, `rickshaw_man()`) are primarily controlled by `BodyDoji`. - **Engulfing-style patterns** (`engulfing()`, `harami()`, `dark_cloud_cover()`, `piercing()`) depend on `BodyLong` and `BodyShort`. - **Star patterns** (`morning_star()`, `evening_star()`, `abandoned_baby()`) depend on `BodyShort`, `BodyLong`, and the shadow settings. - **Hammer/Shooting Star** (`hammer()`, `shooting_star()`, `inverted_hammer()`, `hanging_man()`) primarily depend on `ShadowLong` and `BodyShort`. - **Distance-based comparisons** (`kicking()`, `matching_low()`, `counter_attack()`) use the `Near`, `Far`, and `Equal` settings. ## Charting patterns Candlestick patterns integrate with the [{talib}](https://github.com/serkor1/ta-lib-R/) charting system. Bullish patterns are marked below the candle, bearish patterns above, and direction-neutral patterns use a neutral style: ```{r} { talib::chart(talib::BTC) talib::indicator(talib::doji) talib::indicator(talib::engulfing) } ``` Multiple patterns can be layered on the same chart. Each call to `indicator()` adds its markers to the existing price chart. ## Contributing and bug reports The underlying library, [TA-Lib](https://github.com/TA-Lib/ta-lib), is a long-standing and well-known library but this R wrapper is still in its early stage. All contributions, suggestions, and bug reports are welcome.