Давайте нарисуем простые скользящие средние с помощью Rust

Давайте нарисуем простые скользящие средние с помощью Rust

3 декабря 2022 г.

В этой статье я расскажу и продемонстрирую

  • Простые скользящие средние и их использование
  • Вычисление простых скользящих средних
  • Отображение простых скользящих средних на графике с помощью Rust

Простые скользящие средние и их использование

Если вы занимаетесь трейдингом, возможно, вы использовали или слышали термин «простая скользящая средняя» или сокращенно SMA. Цены активов, которыми мы торгуем, могут колебаться в зависимости от макро- или микрофакторов. С этими колебаниями трудно отслеживать тенденции, которые происходят в режиме реального времени. Из-за этой проблемы методы сглаживания с использованием таких статистических данных, как SMA, предназначены для «сглаживания» ценовых колебаний определенного актива, чтобы мы, инвесторы или трейдеры, могли сосредоточиться на ценовых тенденциях или узоры.

Простые скользящие средние рассчитываются путем получения средней цены закрытия за определенный период времени.

Он использует прошлые данные для указания тренда, поэтому считается запаздывающим индикатором. На фотографии ниже показана простая скользящая средняя за 50 дней с восходящим трендом.

Вычисление простых скользящих средних

Существует несколько способов расчета простых скользящих средних. Я использовал метод вычитания и сложения. Шаблон будет включать в себя получение суммы n, где n — количество дней скользящей средней, а затем взятие этой суммы и деление ее на n. Частное — это SMA дней с 1 по n. Возьмите частное и вычтите его из частного самой старой цены закрытия (n+1), деленного на n. После этого прибавьте ее к частному самой новой цены закрытия, также деленной на n. Паттерн перемещается и повторяется до тех пор, пока вы не дойдете до последней цены закрытия.

Фотография говорит за тысячу слов, так что вот образец. Предположим, мы хотим определить 4-дневную SMA следующих цен закрытия.

рис. а

Теперь давайте перейдем к кодирующей части алгоритма. Вот TLDR или весь код. Позже я объясню детали.

 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);
    }

Первый раздел. Ничего необычного здесь не увидишь. Я только что проверил серию stock_data_series, чтобы убедиться, что она пуста, если нет, то идем дальше. Мне нужно изменяемое хранилище для скользящих средних, поэтому я сделал Vector. Я использовал внешний крейт под названием Decimal, потому что мне нужен удобный крейт для усечения десятичных разрядов и других API в будущем. Также сопоставляется с stock_data_series для извлечения только цен закрытия. Если цены закрытия меньше необходимого количества скользящих средних, то данных будет недостаточно для расчета и будет бессмысленно переходить к следующему участку функции, поэтому я вернул 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;
        }

Здесь мы хотим получить нашу первую простую скользящую среднюю. Как обсуждалось ранее, мы будем использовать эти данные в качестве minuend для следующего раздела. Если вектор цен закрытия равен количеству введенных дней скользящей средней, то мы уже получили то, что нам нужно.

        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);

Отображение простых скользящих средних на графике

После вычисления простых скользящих средних мы можем приступить к их построению на графике. Для этого я буду использовать ящик под названием Plotters. Если вы работали с Python, он очень похож на Mathplotlib. Вот весь код или TLDR для функции. Функция также отображает K-линию, также известную как свечи, внутри графика. Основа кода взята из этого примера плоттеров.

    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)
    }

Функция начинается с проверки того, является ли ряд данных о запасах пустым или нет. stock_data_series содержит цены OHLC (открытие, максимум, минимум, закрытие) для определенного периода времени, в моем использовании период времени находится в пределах 1 дня. Мне также потребовался ввод с именем ma_days, который представляет собой вектор, содержащий фактическое количество дней скользящей средней, которое хотел бы видеть потребитель функции. Если больше 3, вызов функции выдаст ошибку.

Я также поставил стандартную папку, в которой должны сохраняться изображения карт. Он создаст новый, если он не существует. Область рисования похожа на чистый белый холст для нашей диаграммы.

        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)?;

После этого я создал массив объектов-свечей, которые отображают данные OHLC. Если цена закрытия больше цены открытия, то цвет будет зеленым, если наоборот, то красным. From date и To date извлекаются из получения первой и последней записи вектора серии данных о запасах. Ряд данных о запасах организован от самой старой до самой последней записи данных о запасах. Мы будем использовать это в следующем разделе нашего кода.

        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),
        );

<цитата>

В этом разделе кода я извлек минимальную и максимальную цену, которую может предложить серия данных по акциям. Из этих данных я сделал значение y-range. Значение x-range получается из данных "от" и "до". Предоставив эти диапазоны построителю диаграмм, диаграмма теперь может определять положение каждой отдельной свечи.

        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)?;

Этот раздел кода выполняется только в том случае, если вектор ma_days больше нуля, поскольку я сделал построение скользящих средних здесь необязательным. Я использовал здесь карту, чтобы собрать кортеж всех простых скользящих средних, вычисленных на основе каждого заданного дня скользящей средней и входных данных дня скользящей средней. Обратите внимание, что я использовал функцию get_moving_averages на основе раздела Вычисление простых скользящих средних.

        // 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();

Далее я получил начальную дату каждой простой скользящей средней, сформированной из вектора ряда биржевых данных, и с этими данными я связал ее с фактической простой скользящей средней, чтобы сформировать кортеж. Двигаясь дальше, я создал фактический объект серии Line, который представляет наши простые скользящие средние. Каждой серии линий соответствует свой цвет. Я также построил легенду, которая будет отображаться в верхнем левом углу диаграммы.

       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();
            }
        }

Последним штрихом кода является его сборка и сохранение в указанной папке. Функция возвращает true в случае успеха.

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

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

        Ok(true)

Конечный продукт будет выглядеть примерно так.

Заключение

Мы изучили основы расчета простых скользящих средних и построения их графиков с помощью плоттеров, довольно простого технического индикатора для торговли. В будущих статьях мы больше поговорим о финансовых и деловых темах и о том, как мы можем программировать их с помощью Rust или другого языка программирования по вашему выбору.

Код в этой статье — это всего лишь фрагмент библиотеки Rust, над которой я сейчас работаю. Вы можете посетить репозиторий Github здесь. Ура.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE