Hey You! It's Austin Starks, NexusTrade's founder. There was a major bug in an external library we used to calculate technical indicators. Any strategy that used the Relative Strength Index (RSI) indicator will now have WILDLY different backtest results. This article will explain how we identified and fixed the bug.
Consider reading and reacting on Medium to spread my reach!
|
Trusting external libraries can be a double-edged sword. When you come across a library that has garnered 574 stars, 103 forks, and is well-documented and tested, it’s natural to have a high level of trust in its reliability. This is especially true when it’s widely used in the community. However, this trust can be misleading. Can a library, even one this popular and seemingly well-vetted, be completely free of major bugs?
|
I had the same assumption until a startling discovery changed my perspective. In the library I was using for technical analysis, I stumbled upon a significant bug. This flaw led to an entirely inaccurate calculation of the Relative Strength Index (RSI) indicator. The implications of this error are far-reaching, affecting all users of the NexusTrade platform and any other platforms that depend on this library.
|
This article delves into the process of how I identified and subsequently rectified this critical bug.
|
Uncovering the Unexpected: A Different Goal Leads to a Major Discovery
|
Initially, I had no intention of auditing the technical analysis library I was using, trusting in its functionality. My original purpose for delving into the code was entirely different.
|
In the original library, you define a fixed window length. This is useful for backtesting and live-trading assuming your data is ingested at equal time intervals.
|
use ta::indicators::ExponentialMovingAverage; use ta::Next;
let mut ema = ExponentialMovingAverage::new(3).unwrap();
assert_eq!(ema.next(2.0), 2.0); assert_eq!(ema.next(5.0), 3.5); assert_eq!(ema.next(1.0), 2.25); assert_eq!(ema.next(6.25), 4.25);
|
The reality is that expecting evenly distributed data in an algorithmic trading platform is quite a stretch. While this assumption could be forced to fit stock trading scenarios, it became clear that it was not viable when combined with cryptocurrency trading. The cryptocurrency API I was integrating with allowed me to fetch minutely, hourly, and daily cryptocurrency data. And I needed the flexibility to work with dynamically-sized windows.
|
Thus, I decided to fork the repo and fixed the code to work for my particular use case. For example:
|
use ta::indicators::ExponentialMovingAverage; use ta::Next;
let mut ema = ExponentialMovingAverage::new(Duration::seconds(3)).unwrap(); let now = Utc::now();
assert_eq!(ema.next((now, 2.0)), 2.0); assert_eq!(ema.next((now + Duration::seconds(1), 5.0)), 3.5); assert_eq!(ema.next((now + Duration::seconds(2), 1.0)), 2.25); assert_eq!(ema.next((now + Duration::seconds(3), 6.25)), 4.25);
|
This method, which adjusts to variable data intervals, is far more robust than the original fixed window size approach. It enables the creation of a diverse portfolio with various strategies. For instance, with this updated approach, we can seamlessly integrate a portfolio of strategies that includes a 3-day Simple Moving Average and a 30-minute Rate of Change.
|
To reduce the scope, I deleted all technical indicators that weren’t currently implemented in the NexusTrade app. To do the refactor, I used my next-generation AI Configuration Platform, NexusGenAI, to streamline the process.
|
NexusGenAI allows you to create customized AI workflows. The workflow I created was called “TA-Rust-Refactor” and it contained exactly one prompt.
|
The “Refactor Rust” prompt used in the Application
|
While I probably could’ve created a script using NexusGenAI in the backend, I thought it would be faster to simply copy paste the indicators and ask the AI to regenerate the files.
|
The issue became apparent when I reached the final technical indicator in the library, the Relative Strength Index (RSI). The RSI is a momentum indicator commonly used in technical analysis to assess the magnitude of recent price changes. It helps in determining overbought or oversold conditions in the price of a stock or other asset. Typically, it is presented as an oscillator ranging from zero to 100, indicating potential overbought conditions when above 70 and oversold conditions when below 30.
|
After refactoring the RSI Indicator, I couldn’t get the unit tests to pass. Initially, I thought the problem was with NexusGenAI or the OpenAI API. After all, this library was widely adopted, and is present in hundreds of Rust projects. One of the most popular open-source Rust repositories, barter-rs, uses this library in the backend. It definitely seemed more likely that there was a bug in my logic. The unit test was as follows.
|
#[test] fn test_next() { let mut rsi = RelativeStrengthIndex::new(3).unwrap(); assert_eq!(rsi.next(10.0), 50.0); assert_eq!(rsi.next(10.5).round(), 86.0); assert_eq!(rsi.next(10.0).round(), 35.0); assert_eq!(rsi.next(9.5).round(), 16.0); }
|
While the first test case’s discrepancy might be rationalized (since the RSI, being a ratio of average gain to average loss, becomes undefined in the absence of any loss), the results of the final test case were perplexing and inexplicable. I couldn’t discern the logic behind these values.
|
After banging my head against the wall for hours, asking open-source models, and looking up online calculators, I decided to just calculate the Relative Strength Index by hand.
|
1. Calculate the daily price changes:
- Day 2: 10.5 - 10.0 = 0.5 - Day 3: 10.0 - 10.5 = -0.5 - Day 4: 9.5 - 10.0 = -0.5
2. Calculate the average gain and average loss for the 3-day period:
- Average gain: (0.5 + 0) / 3 = 0.1667 - Average loss: (0.5 + 0.5) / 3 = 0.3333
3. Calculate the Relative Strength (RS):
- RS = Average gain / Average loss = 0.1667 / 0.3333 = 0.5
4. Calculate the 3-day RSI:
- RSI = 100 - (100 / (1 + RS)) = 100 - (100 / (1 + 0.5)) = 100 - (100 / 1.5) = 100 - 66.67 = 33.33
So, the 3-day RSI for the given values is approximately 33.33.
|
This is exactly what I was getting in my test case!
|
Doubting my findings, I turned to the GitHub issues for the library. There, my doubts were dispelled when I discovered a reported issue mirroring my experience.
|
Someone discovering that RSI does not match expected values
|
Another user had encountered a discrepancy with the RSI values not aligning with expected outcomes. This confirmation bolstered my confidence in my results. It became clear that my AI Configuration platform had indeed rectified a major bug in a highly popular technical analysis library.
|
This experience teaches us several key lessons. Firstly, the popularity of a library, indicated by hundreds of stars and numerous forks, doesn’t guarantee it’s devoid of bugs. Significant errors like the one uncovered can have wide-ranging consequences for all projects dependent on such a library. This scenario underscores the true essence of open-source work: it allows the community to identify issues and enhance the original work.
|
In light of this, I’ve chosen to keep my version of the technical indicator library open-source. This decision ensures that NexusTrade users can fully understand what’s happening with their portfolios. By making our library open-source, we not only further our goal of democratizing algorithmic trading but also provide our users with utmost transparency regarding the inner workings of their trading strategies.
|
Thank you for reading. If you have technical indicators that you want implemented in the NexusTrade app, feel free to post an issue or submit a pull request!
|
|