Feb 13, 2024 6 MIN

Building a Relative Rotation Graph with OpenBB

To end 2023, everyone was assigned to an internal hackathon. The purpose was to build whatever we wanted with the OpenBB Platform. An opportunity to experience the products from another perspective, use it instead of build it. I love seeing what others come up with, and it's interesting to see how another uses the same tool to interpret the same information.

blog hero

Me, I like a chart because it tells a story. There's a lot of stories to tell, but the one that I wanted to check off of my list was a relative rotation graph.

It's a narrative for momentum and trend among peers, relative to a benchmark, considered a lagging indicator. Not something to daytrade with, but a chart to thin a herd of tickers and visualize the trend.

A common application compares the SDPR Sector ETFs to gauge sector rotation, but what do we actually need to construct the chart?

A relative rotation graph (RRG) is made by plotting the relative strength ratio vs. the relative strength momentum.

The relative strength ratio is calculated by dividing the price of a ticker by its benchmark - AAPL/SPY.

The relative strength momentum is the rate-of-change of the relative strength ratio. Relatively straightforward to calculate, all I need to gather are daily closing prices.

Traders will often cite three factors that matter most to them - price, volume, volatility. RRGs could be a study of any. Price and volume are easy, they come from the same OHLC data, but volatility needs to be calculated. Some logic to handle that process would need to be inserted after the prices are fetched. It's not much extra effort to support those in addition to price, or so I tell myself.

My goals were to:

  1. Create a one-liner function that wrapped fetching data, processing it, and building the chart with minimal inputs required - a list of symbols and a benchmark.

  2. Make it compatible with different sources of data and providers - use the function with supplied data or no data.

  3. Parameterize calculations to allow for different intervals and windows of time.

  4. Return an OBBject where the results attribute is the raw and calculated data used to create a chart for display on demand.

  5. Deploy a Streamlit Cloud app as a live example.

Anything else is gravy.

What I wanted to avoid was making a hot mess of crowded text where data points intersect. It's always going to be an issue with a busy scene, but for my own sanity I wanted to take a shot at improving on that aspect of the presentation.



Collecting the data is pretty easy. Some data providers extensions support multi-ticker downloads, so auto-magically compatible with any of those. The collection point is obb.equity.price.historical().

To prepare the raw data for the next stage and beyond, it needs a little prep work.

It will start like this:


Ending as:


The benchmark (SPY) is kept in a separate DataFrame, formatted the same way.


The first step is to divide the price of each symbol by the price of the benchmark, multiplied by 100. It might be redundant to multiply by 100, but I'll stay true to the norm.


These results are used to calculate the momentum factor.

There are different ways to go about calculating momentum, I decided to go with the 12-1 month method used by Morningstar in their momentum style box.

This means that at least one year of daily data is needed. More history is required to create the "tails" in the RRG, or to study volatility. Three years of daily data is flexible enough to create tail lengths at monthly or weekly intervals without getting too long and making a mess of the chart. It's enough information to determine the direction of travel and overall trend for the period ending on a specific date.

Momentum = ln(1 + r12) - ln(1 + r1)

Where r12 is the rolling twelve month returns and r1 is the rolling one month return.

It's not always about stocks that have 252 trading days per year and 21 per month, so the long and short lengths are parameterized.


After dropping nan values, the three years of data is now two. The final step in the calculations normalizes the ratios and momentum data. I tried a few different normalization techniques - Absolute Max, Min/Max, Z-Score - and found a personal preference for the Z-Score Standardization method.

Here's the ratios: table

Below is the relative strength momentum:



The chart is made with Plotly because it ships with the openbb-charting extension. By keeping things within the OpenBB Platform ecosystem, I'll get the maximum utility with the least amount of effort.

  • The x-axis is the relative strength ratio, the y-axis is the relative strength momentum.

  • Each point on the graph represents a point-in-time.

  • The most important data point is the target date, specified with the date parameter. It should be the main visual element, and the marker colour for each asset should attempt to be dissimilar from one another.

  • There needs to be quadrants divided by the zero lines, and adding some background colours will help focus the narrative - green is good.


There are no real surprises when drawn up at the current date, technology (XLK) is the winner that pulls the benchmark (SPY) with it by gravitational force.

Adding the tails in, covering the length of data, tells the story of the energy sector going full-cycle over two years. It started as a laggard and is on its way to becoming one again.


The tails might be a little distracting at this length though, so a default state might need to be near-term.


Shortening the tails gives a cleaner view of the consumer discretionary (XLY) gaining strength. Take everything with a grain of salt though, Amazon and Tesla make up over 40% of the entire fund on the target date (source: XLY: The Consumer Discretionary Select Sector SPDR® Fund).

A dozen tickers shows pretty well, but colours begin to repeat themselves when looking at the Dow Jones Industrial Average. Creating a large palette of non-similar colours is actually much more challenging than one might expect. That detail, however, can be a problem for another day. Compared against the competition, RRG<GO>, it holds court and that’s a win.



When it came time to build the Streamlit interface, the main challenges became limitations of the service. At least at the free, Community Cloud, level. You can't get everything all at once, or at least shouldn't place those expectations upon yourself.

One major consideration when designing a Streamlit Cloud app is, any time a user interacts with a parameter, the entire application runs from top-to-bottom. What is stored in memory needs to be controlled and contained so that it’s not fetching data on every interaction. The goal is to run the least amount of code possible, and implementing session states will be its saving grace.

A saying among builders goes, "You can have something cheap, fast, and high quality; pick two."

This app and deployment would fall under "fast and cheap", while the underlying function might be "cheap and high quality". I could do a lot more to improve the efficiency of the app, and even just hosting locally is better. Nonetheless, it crosses the finish line to complete my check list of goals. It can only get better now!


Try it out for yourself, here!

Explore the
Terminal Pro

We use cookies

This website uses cookies to meausure and improve your user experience.