Lets Graph Simple Moving Averages Using Rust

Written by 3g015st | Published 2022/12/02
Tech Story Tags: rust | charts | finance | crypto | cryptocurrency | trading | bitcoin | dogecoin

TLDRSimple Moving Averages are calculated by getting the mean closing price over a period of time. It uses past data to indicate a trend, for that reason it is a lagging indicator. via the TL;DR App

In this article, I will discuss and demonstrate

  • Simple Moving Averages and their usage
  • Computing Simple Moving Averages
  • Displaying Simple Moving Averages in a graph using Rust

Simple Moving Averages and their usage

If you are into trading, you may have used or heard of the term ‘Simple Moving Average’ or ‘SMA’ for short. The prices of assets that we’re trading may fluctuate based on macro or micro factors. With those fluctuations, it is hard to look out for trends that are happening in real-time. By that problem, Smoothing techniques using statistics such as SMA are made to “smooth out” the price fluctuations of a certain asset for us investors or traders to focus on price trends or patterns.

Simple Moving Averages are calculated by getting the mean closing price over a period of time.

It uses past data to indicate a trend, and as a result, is considered a lagging indicator. The photo below shows a Simple Moving Average of 50 days with an upside trend.

Computing Simple Moving Averages

There are multiple ways to compute simple moving averages. The method that I used was the Subtraction and Addition Method. The pattern would include getting the sum of n, where n is the number of moving average days, and then taking that sum and dividing it by n, The quotient is the SMA of days 1 to n. Take the quotient and subtract it from the quotient of the oldest closing price (n+1), divided by n. Afterward, add it to the quotient newest closing price, divided by n also. The pattern moves and repeats itself until you get to the last closing price.

A photo speaks a thousand words, so here’s a sample. Suppose we would like to determine the 4-day SMA of the following closing prices.

fig. a

Now, let’s go to the coding part of the algorithm. Here’s the TLDR or the whole code. I’ll explain the bits and pieces afterward.

 pub fn get_moving_averages(&self, ma_days: u16) -> Option<Vec<Decimal>> {
        if self.stock_data_series.len() == 0 {
            return None;
        }

        let mut moving_averages: Vec<Decimal> = vec![];
        let closing_prices = self
            .stock_data_series
            .iter()
            .map(|stock_data| stock_data.close)
            .collect::<Vec<Decimal>>();

        // No moving averages to be computed since current closing price series is not sufficient to build based upon ma_days parameters.
        if closing_prices.len() < ma_days.into() {
            return None;
        }

        let ma_days_idx_end = ma_days - 1;

        let ma_days_decimal = Decimal::from_u16(ma_days).unwrap();
        let mut sum = dec!(0.0);
        for x in 0..=ma_days_idx_end {
            let closing_price = &closing_prices[x.to_usize().unwrap()];
            sum = sum + closing_price;
        }

        let first_moving_average_day = sum / ma_days_decimal;
        moving_averages.push(first_moving_average_day.round_dp(2));

        if closing_prices.len() == ma_days.into() {
            return Some(moving_averages);
        }

        let mut idx: usize = 0;
        let mut tail_closing_day_idx: usize = (ma_days_idx_end + 1).to_usize().unwrap();

        while tail_closing_day_idx != closing_prices.len() {
            let previous_moving_average = &moving_averages[idx];
            let head_closing_day_price = &closing_prices[idx] / ma_days_decimal;
            let tail_closing_day_price = &closing_prices[tail_closing_day_idx] / ma_days_decimal;
            let current_moving_average =
                previous_moving_average - head_closing_day_price + tail_closing_day_price;
            moving_averages.push(current_moving_average.round_dp(2));

            idx += 1;
            tail_closing_day_idx += 1;
        }

        return Some(moving_averages);
    }

First section. Nothing fancy to see here. I just validated the stock_data_series to see if it’s empty, if not then move on. I need a mutable store for the moving averages, so I made the Vector<Decimal>. I used an external crate called Decimal because I need a handy crate to truncate decimal places and other APIs in the future. Also mapped over stock_data_series to only extract closing prices. If closing prices are less than the number of moving averages needed, then the data shall be insufficient for computation and it will be pointless to move forward to the next section of the function, so I returned None.

        if self.stock_data_series.len() == 0 {
            return None;
        }

        let mut moving_averages: Vec<Decimal> = vec![];
        let closing_prices = self
            .stock_data_series
            .iter()
            .map(|stock_data| stock_data.close)
            .collect::<Vec<Decimal>>();

        // No moving averages to be computed since current closing price series is not sufficient to build based upon ma_days parameters.
        if closing_prices.len() < ma_days.into() {
            return None;
        }

What we want to do here is to get our first simple moving average. As discussed previously, we will use that data as a minuend for the next section. If the closing prices vector equals the number of moving average days inputted, then we already got what we need.

        let ma_days_idx_end = ma_days - 1;

        let ma_days_decimal = Decimal::from_u16(ma_days).unwrap();
        let mut sum = dec!(0.0);
        for x in 0..=ma_days_idx_end {
            let closing_price = &closing_prices[x.to_usize().unwrap()];
            sum = sum + closing_price;
        }

        let first_moving_average_day = sum / ma_days_decimal;
        moving_averages.push(first_moving_average_day.round_dp(2));

        if closing_prices.len() == ma_days.into() {
            return Some(moving_averages);
        }

This section of the code is the core part of our function. What it does is it loops through the closing prices until the tail closing day is not equal to the closing prices, as demonstrated in fig. a

        let mut idx: usize = 0;
        let mut tail_closing_day_idx: usize = (ma_days_idx_end + 1).to_usize().unwrap();

        while tail_closing_day_idx != closing_prices.len() {
            let previous_moving_average = &moving_averages[idx];
            let head_closing_day_price = &closing_prices[idx] / ma_days_decimal;
            let tail_closing_day_price = &closing_prices[tail_closing_day_idx] / ma_days_decimal;
            let current_moving_average =
                previous_moving_average - head_closing_day_price + tail_closing_day_price;
            moving_averages.push(current_moving_average.round_dp(2));

            idx += 1;
            tail_closing_day_idx += 1;
        }

        return Some(moving_averages);

Display Simple Moving Averages in a graph

After computing the Simple Moving Averages, we can now proceed on plotting them in a graph. For this, I’ll be using a crate called Plotters. If you come from a Python background, it is very similar to Mathplotlib. Here is the whole code or the TLDR for the function. The function also displays the K-line aka candlesticks inside the chart. The base of the code came from this Plotters example.

    pub fn show_chart(
        &self,
        ma_days: Vec<u16>,
        directory: Option<String>,
        height: Option<u32>,
        width: Option<u32>,
    ) -> Result<bool, Box<dyn Error>> {
        let stock_data_series = &self.stock_data_series;
        if stock_data_series.len() == 0 {
            Err("Insufficient stock data series length")?;
        }

        if ma_days.len() > 3 {
            Err("Exceeded the limit of moving averages to plot")?;
        }

        let dt = Utc::now();
        let timestamp: i64 = dt.timestamp();

        let dir = directory.unwrap_or("chart_outputs".to_string());

        fs::create_dir_all(&dir)?;

        let filepath = format!("{}/{}_candlestick_chart.png", &dir, timestamp);
        let drawing_area =
            BitMapBackend::new(&filepath, (height.unwrap_or(1024), width.unwrap_or(768)))
                .into_drawing_area();

        drawing_area.fill(&WHITE)?;

        let candlesticks = stock_data_series.iter().map(|stock_data| {
            CandleStick::new(
                stock_data.date.date(),
                stock_data.open.to_f64().unwrap(),
                stock_data.high.to_f64().unwrap(),
                stock_data.low.to_f64().unwrap(),
                stock_data.close.to_f64().unwrap(),
                GREEN.filled(),
                RED.filled(),
                25,
            )
        });

        let stock_data_series_last_day_idx = stock_data_series.len() - 1;

        let (from_date, to_date) = (
            stock_data_series[0].date.date() - Duration::days(1),
            stock_data_series[stock_data_series_last_day_idx]
                .date
                .date()
                + Duration::days(1),
        );

        let mut chart_builder = ChartBuilder::on(&drawing_area);

        let min_low_price = stock_data_series
            .iter()
            .map(|stock_data| stock_data.low)
            .min()
            .unwrap();
        let max_high_price = stock_data_series
            .iter()
            .map(|stock_data| stock_data.high)
            .max()
            .unwrap();

        let x_spec = from_date..to_date;
        let y_spec = min_low_price.to_f64().unwrap()..max_high_price.to_f64().unwrap();
        let caption = format!("{} Stock Price Movement", &self.company_name);
        let font_style = ("sans-serif", 25.0).into_font();

        let mut chart = chart_builder
            .x_label_area_size(40)
            .y_label_area_size(40)
            .caption(caption, font_style.clone())
            .build_cartesian_2d(x_spec, y_spec)?;

        chart.configure_mesh().light_line_style(&WHITE).draw()?;

        chart.draw_series(candlesticks)?;

        // Draw moving averages lines
        if ma_days.len() > 0 { 
            let moving_averages_2d: Vec<_> = ma_days
                .into_iter()
                .filter(|ma_day| ma_day > &&0)
                .map(|ma_day| {
                    let moving_averages = self.get_moving_averages(ma_day.clone());

                    match moving_averages {
                        Some(moving_averages) => return (ma_day, moving_averages),
                        None => return (ma_day, Vec::with_capacity(0)),
                    }
                })
                .collect();

            for (idx, ma_tuple) in moving_averages_2d.iter().enumerate() {
                let (ma_day, moving_averages) = ma_tuple;
                let mut ma_line_data: Vec<(Date<Utc>, f64)> = Vec::with_capacity(3);
                let ma_len = moving_averages.len();

                for i in 0..ma_len {
                    // Let start moving average day at the day where adequate data has been formed.
                    let ma_day = i + ma_day.to_usize().unwrap() - 1;
                    ma_line_data.push((
                        stock_data_series[ma_day].date.date(),
                        moving_averages[i].to_f64().unwrap(),
                    ));
                }

                if ma_len > 0 {
                    let chosen_color = [BLUE, PURPLE, ORANGE][idx];

                    let line_series_label = format!("SMA {}", &ma_day);

                    let legend = |color: RGBColor| {
                        move |(x, y)| PathElement::new([(x, y), (x + 20, y)], color)
                    };

                    let sma_line = LineSeries::new(ma_line_data, chosen_color.stroke_width(2));

                    // Fill in moving averages line data series
                    chart
                        .draw_series(sma_line)
                        .unwrap()
                        .label(line_series_label)
                        .legend(legend(chosen_color));
                }

                // Display SMA Legend
                chart
                    .configure_series_labels()
                    .position(SeriesLabelPosition::UpperLeft)
                    .label_font(font_style.clone())
                    .draw()
                    .unwrap();
            }
        }

        drawing_area.present().expect(&format!(
            "Cannot write into {:?}. Directory does not exists.",
            &dir
        ));

        println!("Result has been saved to {}", filepath);

        Ok(true)
    }

The function starts by validating if the stock data series is empty or not. stock_data_series contains OHLC (Open, High, Low, Close) Prices for a specific time frame, in my usage the time frame is within 1 day. I also required an input called ma_days, which is a vector that holds the actual number of moving average days that the consumer of the function would like to see. If it is more than 3 then the function call will throw an error.

I also put a standard folder where the chart images shall be saved. It will create a new one if it doesn’t exist. The drawing area is like a blank white canvas for our chart.

        let stock_data_series = &self.stock_data_series;
        if stock_data_series.len() == 0 {
            Err("Insufficient stock data series length")?;
        }

        if ma_days.len() > 3 {
            Err("Exceeded the limit of moving averages to plot")?;
        }

        let dt = Utc::now();
        let timestamp: i64 = dt.timestamp();

        let dir = directory.unwrap_or("chart_outputs".to_string());

        fs::create_dir_all(&dir)?;

        let filepath = format!("{}/{}_candlestick_chart.png", &dir, timestamp);
        let drawing_area =
            BitMapBackend::new(&filepath, (height.unwrap_or(1024), width.unwrap_or(768)))
                .into_drawing_area();

        drawing_area.fill(&WHITE)?;

Afterward, I created an array of candlestick objects that shows the OHLC data. If the closing price is greater than the opening price then the color will be green, if it’s vice versa then it will be red. From date and To date is extracted from getting the first and last entry of the stock data series vector. The stock data series is organized from oldest to latest stock data entry. We will use this in the next section of our code.

        let candlesticks = stock_data_series.iter().map(|stock_data| {
            CandleStick::new(
                stock_data.date.date(),
                stock_data.open.to_f64().unwrap(),
                stock_data.high.to_f64().unwrap(),
                stock_data.low.to_f64().unwrap(),
                stock_data.close.to_f64().unwrap(),
                GREEN.filled(),
                RED.filled(),
                25,
            )
        });

        let stock_data_series_last_day_idx = stock_data_series.len() - 1;

        let (from_date, to_date) = (
            stock_data_series[0].date.date() - Duration::days(1),
            stock_data_series[stock_data_series_last_day_idx]
                .date
                .date()
                + Duration::days(1),
        );

In this section of the code, I extracted the minimum and maximum price that the stock data series can offer. From that data, I made a y-range value. The x-range value is derived from the from data and the to date. By providing these ranges to the chart builder, the chart can now determine the position of each individual candlestick.

        let mut chart_builder = ChartBuilder::on(&drawing_area);

        let min_low_price = stock_data_series
            .iter()
            .map(|stock_data| stock_data.low)
            .min()
            .unwrap();
        let max_high_price = stock_data_series
            .iter()
            .map(|stock_data| stock_data.high)
            .max()
            .unwrap();

        let x_spec = from_date..to_date;
        let y_spec = min_low_price.to_f64().unwrap()..max_high_price.to_f64().unwrap();
        let caption = format!("{} Stock Price Movement", &self.company_name);
        let font_style = ("sans-serif", 25.0).into_font();

        let mut chart = chart_builder
            .x_label_area_size(40)
            .y_label_area_size(40)
            .caption(caption, font_style.clone())
            .build_cartesian_2d(x_spec, y_spec)?;

        chart.configure_mesh().light_line_style(&WHITE).draw()?;

        chart.draw_series(candlesticks)?;

This section of the code only proceeds if ma_days vector is greater than zero since I made the plotting of moving averages to be optional here. I used a map here to collect a tuple of all of the Simple Moving Averages computed from each moving average day given and its moving average day input. Notice that I used the get_moving_averages function based on Compute Simple Moving Averages section.

        // Draw moving averages lines
        if ma_days.len() > 0 { 
            let moving_averages_2d: Vec<_> = ma_days
                .into_iter()
                .filter(|ma_day| ma_day > &&0)
                .map(|ma_day| {
                    let moving_averages = self.get_moving_averages(ma_day.clone());

                    match moving_averages {
                        Some(moving_averages) => return (ma_day, moving_averages),
                        None => return (ma_day, Vec::with_capacity(0)),
                    }
                })
                .collect();

I proceeded by getting the start date of each Simple Moving Averages formed from the stock data series vector and with this data I partnered it with the actual Simple moving averages to form a tuple. Moving on, I made the actual Line series object that represents our Simple Moving Averages. Each line series corresponds to a different color. I also built a legend that shall be displayed at the upper left corner of the chart.

       for (idx, ma_tuple) in moving_averages_2d.iter().enumerate() {
                let (ma_day, moving_averages) = ma_tuple;
                let mut ma_line_data: Vec<(Date<Utc>, f64)> = Vec::with_capacity(3);
                let ma_len = moving_averages.len();

                for i in 0..ma_len {
                    // Let start moving average day at the day where adequate data has been formed.
                    let ma_day = i + ma_day.to_usize().unwrap() - 1;
                    ma_line_data.push((
                        stock_data_series[ma_day].date.date(),
                        moving_averages[i].to_f64().unwrap(),
                    ));
                }

                if ma_len > 0 {
                    let chosen_color = [BLUE, PURPLE, ORANGE][idx];

                    let line_series_label = format!("SMA {}", &ma_day);

                    let legend = |color: RGBColor| {
                        move |(x, y)| PathElement::new([(x, y), (x + 20, y)], color)
                    };

                    let sma_line = LineSeries::new(ma_line_data, chosen_color.stroke_width(2));

                    // Fill in moving averages line data series
                    chart
                        .draw_series(sma_line)
                        .unwrap()
                        .label(line_series_label)
                        .legend(legend(chosen_color));
                }

                // Display SMA Legend
                chart
                    .configure_series_labels()
                    .position(SeriesLabelPosition::UpperLeft)
                    .label_font(font_style.clone())
                    .draw()
                    .unwrap();
            }
        }

The final touch of the code is to build it and save it inside the specified folder. The function returns true if it is successful.

        drawing_area.present().expect(&format!(
            "Cannot write into {:?}. Directory does not exists.",
            &dir
        ));

        println!("Result has been saved to {}", filepath);

        Ok(true)

The end product will look similar to this.

Conclusion

We have learned the basics of computing Simple Moving Averages and plotting them using Plotters, a fairly simple technical indicator for trading. In future articles, we will tackle more about financial and business topics and how we can program it by using Rust or another programming language of your choice.

The code in this article is just a snippet of the Rust library that I’m currently working on. You can visit the repository Github here. Cheers.


Written by 3g015st | enjoys digesting business & financial information, also a rust hobbyist.
Published by HackerNoon on 2022/12/02