Skip to content

🚀 <xts>-methods implemented - #85

Merged
serkor1 merged 22 commits into
develfrom
impl-xts-methods
Aug 5, 2026
Merged

🚀 <xts>-methods implemented#85
serkor1 merged 22 commits into
develfrom
impl-xts-methods

Conversation

@serkor1

@serkor1 serkor1 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

📚 What?

This PR introduces - and -methods in {talib}. The methods are compatible with with the indicator and charting functions out of the box (See below). It comes with significant changes to the backend to reduce the amount of code needed to implement it.

The PR introduces alot of dead code which will be cleaned up in a separate PR, and many bug-fixes identified during adversial reviews - these will be documented in the NEWS.md separately.

🔥 Proof of Concept

## load data with Quantmod
quantmod::getSymbols(
  Symbols = "SPY",
  auto.assign = TRUE,
  from = as.Date("2023-01-01"),
  to = as.Date("2024-12-31")
)
#> Registered S3 method overwritten by 'quantmod':
#>   method            from
#>   as.zoo.data.frame zoo
#> [1] "SPY"

## check class
class(SPY)
#> [1] "xts" "zoo"

## calculate the bollinger
## bands
print(
  x <- talib::bollingerBands(
    SPY
  )
)
#>            UpperBand MiddleBand LowerBand
#> 2023-01-03        NA         NA        NA
#> 2023-01-04        NA         NA        NA
#> 2023-01-05        NA         NA        NA
#> 2023-01-06        NA         NA        NA
#> 2023-01-09        NA         NA        NA
#> 2023-01-10        NA         NA        NA
#> 2023-01-11        NA         NA        NA
#> 2023-01-12        NA         NA        NA
#> 2023-01-13        NA         NA        NA
#> 2023-01-17        NA         NA        NA
#>        ...                               
#> 2024-12-16  613.0824   600.8815  588.6806
#> 2024-12-17  612.4663   601.6885  590.9107
#> 2024-12-18  613.2155   601.4875  589.7595
#> 2024-12-19  613.9388   601.2675  588.5962
#> 2024-12-20  614.1579   601.1415  588.1251
#> 2024-12-23  614.1925   601.1005  588.0085
#> 2024-12-24  614.2781   601.2890  588.2999
#> 2024-12-26  614.3093   601.3235  588.3377
#> 2024-12-27  614.3693   601.1325  587.8957
#> 2024-12-30  614.7724   600.4160  586.0596

class(x)
#> [1] "xts" "zoo"
options(
  talib.chart.backend = "ggplot2"
)

{
  talib::chart(SPY)

  talib::indicator(
    talib::bollingerBands()
  )

  talib::indicator(
    talib::tradingVolume()
  )

  talib::indicator(
    talib::PVI()
  )
}

Created on 2026-08-05 with reprex v2.1.1

serkor1 added 15 commits August 4, 2026 11:51
* The data is extracted using {quantmod} and is stored
  as an <xts>-object that is to be used for examples, vignettes
  and tests.
* The added class will ease the downstream coding process when adding
  <xts>-methods to the indicators.
* The index refers to the time-index of the object
  passed into the indicator functions.
  By default, for <matrix> and <data.frame>, it (was) is assumed
  that these are given in the rownames() of the passed OHLC-V
  object. For an <xts>-object the time-index is given as the index attribute - that is just
  how <xts> works which necessitates this change.

* With this introduction <xts>, <data.frame> and <matrix> objects can be handled
  in the default method which reduces the number of lines changed for the introduction
  of the <xts>-methods. Any changes and bugs can potentially be handled in the dispatching functions
  like, for example, series.xts() to avoid a mass change in files for a simple bug.
* The signature of the series function is now more "straightforward," in the sense that there are no dispatch switching conditional on how the arguments are passed. This was a change that was long due, as it was a rather confusing construction. The series() is now a more proper S3 function that has a method for each of the relevant data classes.

* A new (classed) method has been introduced to accomodate the <xts> methods in the indicator functions. The method adds an explicit class to the <xts> method such that the <xts> methods can implemented without further changes to the default methods on the indicator side. There have been introduced a wide variety of functions in utils.R to accomodate this change.

* Unit-tests have been rewritten so it fits the new series() structure, which has the same underlying logic but a changed input arguments.
* <xts> methods have been added to all non-rolling indicators (these are considered utility functions)
  which follows the same underlying logic as the <data.frame> and <matrix> methods; ie. it dispatches to
  the default method.
  - The logic are handled by series() and utils.R via S3 classes

* The most "important" change here is that the indicators moves from using rownames to index in the argument naming.
  This is to introduce some sort of consistency across the methods (<xts> does not have any rownmes).
  Under the hood <data.frame> and <matrix> methods still sets the rowname attribute while <xts> sets the index attribute
  of the resulting indicator.
* series.xts() now resolves each formula variable to exactly one column
- exact name matches first, then quantmod-style suffix matches
(close -> TICKER.Close), both case-insensitive and regex-free - and
returns the columns in formula order, so the C layer receives
high/low/close/... in the positions the indicator expects regardless
of the physical column layout of the <xts>.

* This fixes silently wrong values for multi-input indicators on
reordered or substring-colliding column layouts, honors explicit
'cols =' selections exactly, errors on missing or ambiguous columns
instead of computing on arbitrary ones, and accepts lowercase
open/high/low/close names.

* Unused model.frame-arguments ('subset',
...) now warn instead of being silently dropped. Chart data overrides
with quantmod-named <xts> (indicator(RSI, data = x)) resolve through
the same path and now work.
* Every .xts method now calls assert_xts(): the {xts} namespace is
loaded on demand via requireNamespace(), with a clear error when the
package is not installed. library(talib); RSI(GOOGL) works in vanilla
sessions - and in zoo-only sessions - instead of dying with
'subscript out of bounds'; the GOOGL man-page example runs under
R CMD check again.

* set_index() gained a default method with a clear message, replacing
the bare 'no applicable method' failure on malformed <xts>-classed
input.

* trading_volume() received the xts rollout the generated wrappers
already had: index()/set_index() plumbing, a trading_volume.xts
method, and the <ta_object> class stamp in src/volume.c that every
generated C wrapper attaches via wrapper.h - VOLUME(xts) returns a
proper <xts> carrying the input's index and the actual volume column.
trading_volume.matrix now wraps in as.matrix() like the template, so
matrix output stays a plain <matrix>.

* Regenerated wrappers via make codegen; the only generated diff is
the assert_xts() guard in each of the 129 .xts methods.
* All seven rolling generics (rolling_maximum, rolling_minimum,
rolling_sum, rolling_standard_deviation, rolling_variance,
rolling_beta, rolling_correlation) gained an .xts method: a
single-column <xts> in, an <xts> out carrying the input's index and
the statistic's column name (MAX, BETA, ...), with values identical
to the vector path. For the bivariate pair, 'y' may be a vector or a
single-column <xts>; pairing is positional.

* Multi-column input is now rejected with a clear error ('Expected
'x' to be univariate.') instead of being silently flattened
column-major into one concatenated series - the guard is generated
per series argument into .default and .xts, covering xts and matrix
input alike. This matches TTR::runMax's univariate contract, and
runMax/MAX now agree exactly on identical input.

* Vector returns from the rolling family drop the stale
c('ta_object','matrix','array') class alongside the dim-strip - a
dimensionless vector no longer claims inherits(., 'matrix') - while
keeping the lookback attribute.

* Wiring: ${SERIES_GUARD} placeholder in render.rs,
rolling_template.R .xts block, generated xts value-parity and
multivariate-error test blocks in testthat.rs (guarded by
skip_if_not_installed('xts')), and the rolling man-roxygen templates
now document the xts contract. Also corrected the template's
fresh-render call site (${C_NUMERIC},,, -> ${C_NUMERIC},), a latent
syntax bug masked by splice preservation.
* The .numeric methods strip the internal class unconditionally:
single-output results are plain double vectors (dim and class
dropped), multi-output results are plain matrices carrying the
implicit c('matrix','array') class - the C-stamped
c('ta_object','matrix','array') tag no longer leaks out of the
vector fast path. Column names and the lookback attribute are kept,
inherits(x, 'matrix') finally agrees with is.matrix(x), and base
as.matrix()/as.data.frame() coerce the results correctly.

* Unknown arguments landing in '...' on the numeric path now warn
("'...' is passed but is unused for vectors.") instead of being
silently swallowed - the only signal a case-typo'd parameter name
(timeperiod = 10) will ever produce. Passing 'cols' keeps its
existing warn-and-compute behavior.

* Generated numeric tests lock the new shapes:
expect_false(inherits(x, 'matrix')) for single-output,
expect_identical(class(x), c('matrix', 'array')) for multi-output.
@serkor1 serkor1 linked an issue Aug 4, 2026 that may be closed by this pull request
4 tasks
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.98734% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.94%. Comparing base (2f20763) to head (aae378f).

Files with missing lines Patch % Lines
R/utils.R 73.13% 18 Missing ⚠️
R/ta_VOLUME.R 67.56% 12 Missing ⚠️
R/helper.R 61.90% 8 Missing ⚠️
R/series.R 95.31% 6 Missing ⚠️
R/chart_build.R 50.00% 4 Missing ⚠️
R/ta_VWMA.R 86.66% 4 Missing ⚠️
R/chart_indicator.R 89.47% 2 Missing ⚠️
src/names.c 71.42% 2 Missing ⚠️
R/ta_APO.R 97.05% 1 Missing ⚠️
R/ta_AVGDEV.R 96.42% 1 Missing ⚠️
... and 31 more
Additional details and impacted files
@@            Coverage Diff             @@
##            devel      #85      +/-   ##
==========================================
- Coverage   96.01%   95.94%   -0.07%     
==========================================
  Files         162      162              
  Lines       19716    21676    +1960     
==========================================
+ Hits        18930    20797    +1867     
- Misses        786      879      +93     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

serkor1 added 7 commits August 4, 2026 20:02
* tests/testthat/helper-library.R attaches {xts} only when it is
installed, and every generated <xts> test block carries
skip_if_not_installed('xts') - the suite no longer hard-depends on a
Suggests package (verified against a library with xts/zoo hidden:
everything loads, the xts blocks skip cleanly, zero failures).

* Every generated test file gained a 'Value parity with <data.frame>
(<xts>)' block: the <xts> result must equal the lowercase
<data.frame> reference numerically, both on the GOOGL fixture and on
an alphabetically sorted copy that places 'Adjusted' before the OHLCV
columns - locking the column-resolution contract (formula order,
suffix matching, no substring collisions) for all 129 indicators.

* test-series.R gained unit tests for the <xts> resolution layer
itself: formula-order selection, quantmod-style suffix matches,
lowercase names, explicit 'cols =' order, and the error/warning
contracts (no match, ambiguity, short formula, unused 'subset');
plus a vanilla-session subprocess regression (skip_on_cran) proving
RSI(GOOGL) works without {xts} attached.
* series() gained a default method that defers any classed input to
as.data.frame() before column resolution - a multivariate zoo (or any
data.frame-coercible object) now computes on the coerced frame with
the index as rownames, instead of dying with the internal
"no applicable method for 'series'" error. Uncoercible inputs fail
with base R's own coercion message naming the class.

* The indicator documentation now states the actual per-class
contract: xts in, xts out alongside data.frame/matrix; coercible
inputs computed on the coerced data.frame; the xts column resolution
(exact matches first, then quantmod-style suffix matches, consumed in
formula order) documented under 'cols'; and '...' marked unused for
xts input.

* index_data_frame(), index_matrix() and index_xts() in src/names.c
now return SEXP (R_NilValue) as Writing R Extensions requires for
.Call routines - previously void, an undefined-behavior read of the
return register. A registration sweep confirms no other void .Call
routines remain.
* The chart dispatch no longer calls ...names() (R >= 4.1): the
argument names are already captured by the substitute() call one line
above, so names(dots_quoted) carries the identical information.

* nullfile() (R >= 4.0) is replaced by an internal .nullfile() in the
small base-R replacements section of utils.R, returning the platform
null device; the three render-path call sites in chart_indicator.R
now use it. Verified against the single-panel print, the multi-panel
grob alignment and the print-guard paths.

* The pattern-marker data.frames in chart_pattern.R set
stringsAsFactors = FALSE explicitly - the default flipped in R 4.0,
and the declared floor predates the flip.

**NOTE:** This was captured during an adversial review by Fable.
* trading_volume() on a vector no longer errors on the default
maType: the numeric splice converts the specifications like .default
does.
* 'subset' is evaluated exactly once, by model.frame(), identically on
every route (direct data.frame call, wrapper-on-chart, indicator()):
data-context expressions like subset = close > 60000 now work
everywhere, columns of the data win over caller objects, and the
caller's frame is the fallback. The internal re-evaluations that
crashed data-context expressions - and evaluated side-effecting
expressions up to three times - are gone; the subset attribute now
records the row positions model.frame() actually kept.

* Arguments on the chart path resolve in the caller's frame:
data =/subset = referencing function-local objects work from inside
function bodies. indicator() keeps 'subset' quoted and evaluates the
wrapper call in the caller's frame, so all routes share one semantic.

* The ggplot2 backend aligns indicator positions by axis label,
mirroring plotly - indicators computed with subset = are drawn at the
subset's own candles instead of positions 1..n.

* indicator(FUN, data = y) no longer silently stamps the chart's
dates onto a mismatched override: a length mismatch warns and the
trace is labelled with the override's own rownames.

* chart() normalizes quantmod-style OHLCV names (TICKER.Close, Close)
to the lowercase names the backends consume - exact matches first,
then dot-suffix matches, mirroring series.xts() - and fails fast with
the package's own missing-columns message instead of an opaque
backend error at render time. Standalone indicator() with <xts> data
keeps its time axis.
* Added <xts>-method mention, and removed the minimal
  dependency argument - even though xts and zoo are suggested
  it is still a departure from the minimal dependency framework.

* Replaced BTC with GOOGL to demonstrate <xts> compatibility with
  class in and class out condtion.
@serkor1 serkor1 added the enhancement New feature or request label Aug 5, 2026
@serkor1
serkor1 merged commit 57804ee into devel Aug 5, 2026
26 of 28 checks passed
@serkor1
serkor1 deleted the impl-xts-methods branch August 5, 2026 04:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚀 Consider {xts} support

1 participant