--- title: "Time-Series-dygraph" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Time-Series-dygraph} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} suppressPackageStartupMessages(require(dplyr)) suppressPackageStartupMessages(require(tidyquant)) suppressPackageStartupMessages(require(FinanceGraphs)) ``` ```{r,echo=FALSE} knitr::opts_chunk$set(fig.width = 7) options(datatable.print.keys=FALSE, datatable.print.class=FALSE) ``` # Interactive Time Series Visualization with Dygraphs Visualizing financial time series is critical to communicating ideas and deciding how to trade or invest. While [ggplot2](https://ggplot2.tidyverse.org/) is the gold standard for static graphs, [Dygraphs](https://rstudio.github.io/dygraphs/index.html) is an excellent package to mix interactivity with flexibility and visual aesthetics, and is well suited to Financial time series in particular. That flexibility is great, but can take some time (and code) to set up and customize well. This package is designed to reduce the time spent going from data to finished and enhanced visualization. Rather than emphasize pipes and complicated coding, this package offers just a few flexible functions and a simplified parameter-based approach to graph customization. Aesthetic details have sensible defaults, but are almost always customizable. The package is designed to be as agnostic as possible about input data, and also seeks to manage other *common data* which may add useful information to the graph. # Data vs Time plots with `fgts_dygraph` `fgts_dygraph()` is a flexible wrapper around [dygraphs](https://rstudio.github.io/dygraphs/index.html) building blocks. Internally, it makes extensive use of `data.table` functionality, both for flexibility and speed. Key concepts useful to use this function are - **Input data** is always a `data.frame` or `data.table` with a `Date` coercible column. It can either be narrow/long, with a character variable such as `variable` (needed as a parameter if different) for series names and a single numeric column, or wide, with a `Date` column and multiple numeric columns whose names are used as series. - **Events** are annotations added to the graph with a date or date range associated with it. There are several "event helpers" provided which help to find and map various types of annotations to a common format used internally within the function. The most common examples are **dates of interest** where market moving events or regime periods may be added. - **Annotations** are notes or annotations on axes other than the date axis. Examples (shown below) include lines which show the last values or series names at their endpoints, or ranges highlighting (e.g.) buy or sell targets. - **Range highlighting** are options to focus the graph on subsets of the plotted data. ## Simple Examples Most financial time series are just prices of various assets. Most of the examples will use a simple prepared data set of prices. Simply plotting these prices can be done with just a few parameters. ```{r} head(eqtypx,1) fgts_dygraph(eqtypx, title="Stock Prices", ylab="Adjusted Close") ``` Note two important features of this graph. First, the series is smoothed by taking moving averages specified by the number in the lower left hand column. These values are determined by the length of the input series, can be changed using the `roller` parameter. Second, there is a nice date range selector below the graph. This can be interactively used to focus on parts of the graph, and double clicking within the graph always resets the view to the broadest possible one. Let's enhance the graph with a few more parameters that can be added to the `fgts_dygraph` function call: ```{r} fgts_dygraph(eqtypx, title="Stock Prices, Date focused", roller=3,dtstartfrac=0.6,splitcols=TRUE) ``` | Parameter call | Feature | |:------|:-----------------------------| |`roller=3`|Lessen the smoothing by only averaging every 3 prices | |`dtstartfrac=0.6`|Start the graph 60 percent of the way from input series beginning to the end | |`splitcols=TRUE`|Split the first series found (leftmost if wide, first if narrow) onto a second axis. A list of series names can also be given|" To highlight or hide series and focus in on a particular date range, use: ```{r} fgts_dygraph(eqtypx, title="Stock Prices, highlighted", roller=1, dtwindow="-3y::",hidecols="TLT",hilightcols="IBM",hilightwidth=4, hilightstyle="dashed") ``` |Parameter call|Feature| |:------|:--------------------| |`dtwindow="-3y::"`|focus the date range selector to 1 year prior to `Sys.Date()`| |`hidecols="TLT"`|Hide one or more series. Can either separate by `;` or give a list.| |`hilightcols="IBM`|Highlight a series by thickening its line or changing the line style| |`hilightwidth=4`|New width for columns named in`hilightcols`| |`hilightstyle="dashed"`|New line style for columns named in`hilightcols`| ## Horizontal annotations and rebasing Many times, you may want to show relative changes of series with widely varying values. [Dygraphs](https://rstudio.github.io/dygraphs/index.html) has a nice function `dyRebase` for doing so, but there are times you would like to rebase to given date. For example to make all the series start at 120 as of the beginning of 2025, use the `rebase` parameter. There are also ways to add horizontal lines to highlight current (or last in the dataset) values or labels. These can be useful when you prefer more static graphs than interactive ones, especially when legends are turned off. |Parameter call|Feature| |:------|:--------------------| |`rebase="2025-01-01,120"`|Divide all series by their values a of 1/1/2025 and scale to 120.| |`annotations="last,linelabel"`|Put vertical lines highlighting last values in each series | |`annotations="range,100,120"`|Put a highlighted range betwewen 100 and 120 percent of value on 1/1/2025| |`events="pt,2025-02-01,QQQ,Feb1"`|Add a note a particular date and series| |`dylegend="never"` |Don't replicate information in the legend| |`bg_opts="grid,none;norange"`|Hide the grid and the date range selector| ```{r} fgts_dygraph(eqtypx, title="Stock Prices, rebased",hidecols="TLT;EEM", dtstartfrac=0.7, rebase="2025-01-01,120",events="pt,2025-02-01,QQQ,Feb3", annotations="last,linelabel;range,100,120", dylegend = "never", bg_opts="grid,none") ``` - Note that column specifications and annotations are flexible, i.e. `hidecols="TLT;EEM"` or `hidecols=c("TLT","EEM")` can also be used, or annotations strung together with semicolons. - Also, notice that focusing in the displayed date range reduces the roller smoothing parameter appropriately. However, until "1" is put in the lower left corner box, the series will still be smoothed and the last value won't agree with the `last,linelabel` annotation. - The data series have the colors same colors they would have if they were not hidden by `hidecols`. To draw with the colors typically used for the first two series (see Customization below), take the series out before sending to `fgts_dygraph()` by (e.g.) using instead `fgts_dygraph(eqtypx |> dplyr::select(date,IBM,QQQ),...)` to put those two series first. - Dates used for events `"pt"` (and `"dt"`) are rolled to next dates if not in the underlying data. ## Grouping series together Many time series are closely related, as in confidence intervals around a forecast or ranges for a given time periods. [Dygraphs](https://rstudio.github.io/dygraphs/index.html) allows you to plot series with areas shaded between lower and upper bounds. The ability to do so can be used to enhance graphs in many ways. Some I've used in addition to adding confidence bounds include * Positioning, momentum, or sentiment as (proportional) shaded areas underneath or over a series. * VAR bounds for unit investments. * Statistical significance to exogenous time series. * Evolution of peer comparisons through time. `fgts_dygraph()` plots together series which have suffixes in (`.lo`,`.hi`) with their undecorated counterparts, keeping their assigned colors. An example showing Colombia's currency value relative to peers is: ```{r} toplot <- reerdta[REGION=="LATAM",.(cop=sum(value*(variable=="COL")), peers=mean(value),peers.lo=min(value),peers.hi=max(value)),by=.(date)] head(toplot,2) ``` Those series (`peers`, `peers.lo`, and `peers.hi`) are combined to get a graph which shows how one series has moved with a geographically similar set of peers. ```{r} fgts_dygraph(toplot,title="COP REER vs Latam peers",ylab="Price", roller=1,hilightcols="cop",hilightwidth=4,annotations="last,linevalue") ``` Another example is showing periods where a series is significantly correleted to another (e.g. the broader market). The series `eqtyrtn` in the package has the rolling 66 day `p.value` of regressing returns of `TLT` against `QQQ`: ```{r} toplot <- eqtypx |> left_join(select(eqtyrtn,date,p_TLT_QQQ), by="date") |> filter(!is.na(p_TLT_QQQ)) |> transmute(date,TLT, TLT.lo=TLT * case_when( p_TLT_QQQ<0.04 ~ 0.9,.default=1) ) fgts_dygraph(toplot,title="TLT with significant relationship to QQQ",ylab="Price",xlab="shaded areas significant",roller=1) ``` ## Events and Event handlers `fgts_dygraph()` has many ways of integrating date-based information such as events or regimes with the original data. Both "pre-canned" events and events which draw in other information (via `event_ds`) can be displayed. Events are show in two ways: - Single day events are shown as horizontal lines with a text annotation on the date. - Multi day events (which have both a `date` and `date_end`) are shown as shaded bands between two dates. The colors of the bands are designed to be consistent with their meaning, i.e. green for positive or red for negative. Those colors are customizable. ### One liners using `events` parameter Events can be determined from several sources, and can be combined together. - **Internal data**: The package maintains contains a prepackaged data set of historical *market regimes* or other events of interest (called "dates of interest" or just "doi"). Those can be added to (with new categories) or amended as needed, and then added to the graph using (e.g.) `events="doi,fedmoves"`. Two categories included with the package are `fedmoves` with individual dates and `regm`, which has both beginning and end dates. ```{r} smalldta <- eqtypx |> filter(date>=as.Date("2023-01-01")) |> select(date,EEM,TLT) fgts_dygraph(smalldta,title="With Precanned Events",ylab="Price",rebase=",100",roller=1, events="doi,regm;doi,fedmoves") ``` Internally, a **persistent** data set is kept that includes dates (beginning and/or end), text labels (as `eventid`) and formatting information. In some cases the formatting is inferred from the label (e.g. ending in "+" makes it green), but each point in customizable. ```{r} fg_get_dates_of_interest("fedmoves|regm") |> group_by(category) |> slice_tail(n=2) ``` New data can be added easily. We can add new events (e.g a FOMC move in 2026) by using the `fg_update_dates_of_interest()` function. ```{r} newdoi <-data.frame(category="fedmoves",eventid="F:-50",DT_ENTRY=as.Date("6/16/2026",format="%m/%d/%Y")) fg_update_dates_of_interest(newdoi) fg_get_dates_of_interest("fedmoves") |> dplyr::slice_tail(n=2) |> as.data.frame() ``` - **Seasonal factors** : Regularly recurring dates can also be added. They can either be absolute, (e.g. equity option expirations or IMM CDS roll dates) or relative to the end of the series, (e.g. dates with same day of the quarter or business day of the year.) See documentation for details. - **Single dates**: Coercible dates can also be added with a simple string of the form `"dt,,yyyy-mm-dd"`. For example, to add monthly option expiration dates and a note for Christmas, add ```{r} fgts_dygraph(smalldta,title="With Seasonals",rebase=",100",roller=1,dylegend="never",dtstartfrac=0.7, events="seasonal,optexp,mo|qtr;dt,XMAS,2025-12-25") ``` - **Statistically determined dates** can be added. There are few available with one-line specification, but most can be added with "helpers" discussed in the next section. Breakouts, extrema, and turning points are currently implemented. When called through the `events` line, they operate on the first series found. For example, 7 turning points on `QQQ` are plotted with ```{r} fgts_dygraph(smalldta,title="With Turning Points",rebase=",100",roller=1,events="tp,7") ``` ### Events, Annotations, and forecasts can all be added with separate data frames. Events can also be added with `data.frames` that include all the details of the annotation, such as the text, dates, and colors. Those can be created by the user, but the package includes several "helpers" to take (more) raw data and add relevant formatting details. ### Event helpers Event helpers are small functions which convert any time based data into the correct annotations. The currently implemented helpers are |Function|Description| |:---|:------------| |`fg_addbreakouts()`|Statistically identify breakout points (also available via `events`) | |`fg_findTurningPoints()`|Statistically identify turning points (also available via `events`) | |`fg_cut_to_events()`|"Cut" a univariate series into colored bands, with two different colors for positive and negative values | |`fg_signal_to_events()`|Map a long/short signal to events | |`fg_tq_divs()`|Add dividend events from [tidyquant](https://business-science.github.io/tidyquant/) dividend data | |`fg_av_earnings()`|Add earnings events from [alphavantagepf](https://github.com/derekholmes0/alphavantagepf) earnings data | |`fg_ratingsEvents()`|Add colored ranges based on analyst credit ratings | The first two are described in the function documentation. The next two are designed to map exogenous univariate series to colored regions. - `fg_cut_to_events()` creates event series whose colors (1) vary with the "strength" of the signal, and (2) use two different colors for positive and negative values. Suppose we want to overlay a sense of consumer sentiment data over equity prices. The following code combines both `QQQ` and sentiment one graph (so you can see what's happening) and the coloring that results from the sentiment data. Note that we use a long/melted format for the data. ```{r} toplot <- rbind(eqtypx_melt |> filter(variable=="QQQ"), consumer_sent |> transmute(date,variable=symbol,value=price)) fgts_dygraph(toplot,title="With consumer sentiment",splitcols="UMCSENT",stepcols="UMCSENT",roller=5, event_ds = fg_cut_to_events(consumer_sent,center="zscore")) ``` - `fg_signal_to_events()` uses run-length encoding to map a discrete signal series to a set of colors. This would be helpful with a long/short signal overlaid on an asset price. The following example shows a simple moving average strategy (with periods of no positioning) overlaid on EEM (Emerging Markets Equity ETF). Note how easy it is to make these with `data.table`. First we create the signal, and then get some colors to map to the labels. (For more on `fg_get_aes()` see below) ```{r} suppressPackageStartupMessages(require(data.table)) ma_signal<-eqtypx[,.(date,sig=cut(frollmean(EEM,5)-frollmean(EEM,20), c(-10,-0.5,0.5,10),labels=c("long","flat","short")),EEM)] tail(ma_signal,3) ``` ```{r} colormap <- fg_get_aes("tradesignal")[,.(sig=variable,value)] colormap ``` ```{r} fgts_dygraph(eqtypx[,.(date,EEM)],dtstartfrac=0.6,roller=1,title="5/20 MA positions", event_ds=fg_signal_to_events(ma_signal,colormap)) ``` - `fg_tq_divs()` and `fg_av_earnings()` create event datasets of dividend and earnings information. The dividends require [tidyquant](https://business-science.github.io/tidyquant/). Since there are many ways to get earnings data, the earnings helper needs the data to be downloaded before invocation. A sample set of IBM earnings is included in the package, but you can also use the commented code to get the same thing. ```{r} suppressPackageStartupMessages(require(tidyquant)) all_events <- rbindlist(list(fg_tq_divs(c("IBM")),earnings_ibm |> fg_av_earnings() )) fgts_dygraph(eqtypx[,.(date,QQQ,IBM)],title="With earnings and divs",dtstartfrac=0.8,event_ds=all_events) ``` Note that mutiple event sets can be used with `rbind`, and the color of the event annotations matches that of the original series. - `fg_ratingsEvents()` maps a `data.frame` of ratings changes to colored bars which get darker as the ratings move nearer to the High-Yield/Investment grade divide. The package has a very abbreviated set of ratings changes (`ratings_db`) which, when overlaid with a currency (or spread), gives ```{r} head(ratings_db,3) fgts_dygraph(nomfxdta |> filter(variable=="COP"),title="COP with Ratings", event_ds=fg_ratingsEvents("COLOM",ratings_db,agency="S.P")) ``` ## Forecasts Time series plotted in `fgts_dygraph` can also be extended beyond the current day. Forecasts come in many output forms, but fortunately, there are `broom` like objects now which can standardize the outputs of many forecasting models. [forecast](https://pkg.robjhyndman.com/forecast/) in particular can produce standardized forecasts from multiple models, including `ets()`, `auto.arima()`, `tbats` and a host of others. The forecasts from `forecast` require quite a bit of post-processing to recover dates, but fortunately the function `sw_sweep()` from [Sweep](https://business-science.github.io/sweep/articles/SW00_Introduction_to_sweep.html) produced an actual `data.frame`. This can be forwaded to another (included) helper `fg_sweep()`. An example to predict QQQ with `ets()` is shown below. First, we show the format we expect the forecasts to be in. ```{r} suppressPackageStartupMessages(require(timetk)) suppressPackageStartupMessages(require(forecast)) suppressPackageStartupMessages(require(sweep)) fcst_eqtypx <- tk_ts(eqtypx[,.(date,QQQ)]) |> ets() |> forecast::forecast(h=60) |> sweep::sw_sweep(timetk_idx=TRUE) head( fcst_in <- fg_sweep(fcst_eqtypx) ,3) ``` ```{r} fgts_dygraph(eqtypx[,.(date,IBM,QQQ)],title="Rebased With Forecasts",roller=1,dtstartfrac=0.6,rebase="2024-01-01,100",forecast_ds=fcst_in) ``` Note that - The color of the forecast corresponds to the color of the original series, with a dotted line for the actual forecasts. - Forecasts are rebased with the original series. ## Customization: Colors The visual aesthetics of `fgts_dygraph`s are designed to have sensible defaults, but still somewhat customizable, and more importantly mostly out of the way of the function call. Colors in particular are stored internally in *persistent* `data.frames` To see the default line colors and shading, use `fg_get_aes()` as seen below, or run `fg_get_aes()` to get a chart with the actual colors used. Some colors have `variables` associated with them, which are arbitrary ordering values or (in some cases) values mapped to greps on the text labels. [\^1] ```{r} mycolors <-fg_get_aes("lines",n_max=4) mycolors ``` [\^1] In particular, the included events in the set `regm` have text labels that look like (e.g) `"TariffT-"`. The "-" at the end of the text is mapped to the `marketregines,-` color. There are two ways to change colors. To persistently change them use `fg_update_aes()` which requires a `data.frame` in the same format as that obtained with `fg_get_aes()`. To use graduated colors instead of the defaults chosen by the package: ```{r} suppressPackageStartupMessages(require(RColorBrewer)) newcolors <- mycolors |> mutate(value = rev(RColorBrewer::brewer.pal(8,"GnBu"))[1:4]) fg_update_aes(newcolors, persist=TRUE ) fg_get_aes("lines",n_max=4) ``` which gives, with annotations ```{r} fgts_dygraph(eqtypx, title="Stock Prices, new colors", annotations="last,label",rebase=",100",bg_opts="grid,none") ``` To just change series colors temporarily in a session (and not save them for future use, you can use `fg_update_line_colors()` as in the next example ```{r} fg_update_line_colors(c("gray30","gray30","red","gray30")) fgts_dygraph(eqtypx, title="Stock Prices, line colors", annotations="last,label",rebase=",100",bg_opts="grid,none") ``` To reset the colors back to their defaults, run ```{r} fg_reset_to_default_state("color") fg_get_aesstring("lines",n_max=2) ``` ## Customization: Adding Dates of Interest Events added with the `events="doi,"` parameter can managed persistently across invocations of the package by using `fg_update_dates_of_interest()`. For example, suppose the FOMC cuts rates 50bps in the future. The event can be added with ```{r} newdoi <-data.frame(category="fedmoves",eventid="F:-50",DT_ENTRY=as.Date("6/16/2026",format="%m/%d/%Y")) fg_update_dates_of_interest(newdoi) tail(fg_get_dates_of_interest("fedmoves"),2) |> as.data.frame() ``` Entire new categories can be added. Here are a few examples: ### Recession Indicators Adding recession indicators from [FRED](https://fred.stlouisfed.org/series/RECPROUSM156N). FRED has a monthly probability of recession indicator. Mapping that indicator to a yes/no series using a 7pct threhold, we can add recession events using the event helper `fg_signal_to_events()`: ```{r} recindic <- recession_indic |> transmute(date,isrec=(price>7)) colormap <- data.frame(isrec=c(FALSE,TRUE),color=c("white","pink"),eventid=c("","Recession")) newevents <- fg_signal_to_events( recindic, colormap |> mutate(category="FREDPREC")) fg_update_dates_of_interest(newevents) fgts_dygraph(eqtypx, title="Stock Prices, w/ Recession Events", events="doi,FREDPREC",rebase=",100",bg_opts="grid,none") ``` ### Central Bank events We can add other central bank moves, e.g. Brazil's COPOM decisions [BCB](https://www.bcb.gov.br/en/monetarypolicy/historicaldata), by downloaded the data and using ```{r} filepath <- system.file("extdata", "selic_historical_rates.csv", package = "FinanceGraphs") selic<-data.table::fread(filepath,skip=1,select=c(2,5),col.names=c("date","tgt")) selic<-selic[,let(DT_ENTRY=as.Date(date,format="%m/%d/%Y"))][order(DT_ENTRY)][,move:=c(0,diff(tgt,1))] newevents <- selic[,.(category="COPOM",eventid=format(tgt,digits=2), DT_ENTRY,color=c("blue","grey","red")[sign(move)+2])] fg_update_dates_of_interest(newevents[!color=="grey"]) # Only add real moves tail(newevents,2) ``` and use it easily with just a text argument to `events`. ```{r} fgts_dygraph(filter(nomfxdta,variable=="BRL"),title="BRL w/ COPOM",dtstartfrac=0.6,events="doi,COPOM") ``` Cleaning up any persistent event additions can be accomplished with ```{r} fg_reset_to_default_state("all") ```