longbridge-candlesticks 0.2.61

Longbridge candlestick utils for Rust
Documentation
use rust_decimal::{prelude::FromPrimitive, Decimal};
use time::{macros::time, Date, Duration, Month, OffsetDateTime, Time, Weekday};
use time_tz::OffsetDateTimeExt;

use crate::{market::UpdateFields, Market, Period, Type};

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Candlestick {
    pub time: OffsetDateTime,
    pub open: Decimal,
    pub high: Decimal,
    pub low: Decimal,
    pub close: Decimal,
    pub volume: i64,
    pub turnover: Decimal,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Trade<'a> {
    pub time: OffsetDateTime,
    pub price: Decimal,
    pub volume: i64,
    pub trade_type: &'a str,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum UpdateAction {
    UpdateLast(Candlestick),
    AppendNew(Candlestick),
    None,
}

pub trait IsHalfTradeDay: Copy {
    fn is_half(&self, date: Date) -> bool;
}

impl IsHalfTradeDay for bool {
    #[inline]
    fn is_half(&self, _date: Date) -> bool {
        *self
    }
}

pub struct Merger<T> {
    market: Market,
    period: Period,
    is_half_trade_day: T,
}

impl<T> Merger<T>
where
    T: IsHalfTradeDay,
{
    #[inline]
    pub fn new(market: Market, period: Period, is_half_trade_day: T) -> Self {
        Self {
            market,
            period,
            is_half_trade_day,
        }
    }

    fn round_time(
        &self,
        mut time: OffsetDateTime,
        trade_sessions: &[(Time, Time)],
    ) -> OffsetDateTime {
        for (idx, (start, end)) in trade_sessions.iter().enumerate() {
            if time.time() < *start {
                time = if idx == 0 {
                    time.replace_time(*start)
                } else {
                    time.replace_time(trade_sessions[idx - 1].1)
                };
                break;
            } else if time.time() < *end {
                break;
            } else if idx == trade_sessions.len() - 1 {
                time = time.replace_time(*end);
                break;
            }
        }

        time
    }

    pub fn candlestick_time(&self, ty: Type, time: OffsetDateTime) -> OffsetDateTime {
        let Merger {
            market,
            period,
            is_half_trade_day,
        } = self;
        let trade_sessions = if !is_half_trade_day.is_half(time.date()) {
            market.trade_sessions(ty)
        } else {
            market.half_trade_sessions(ty)
        };
        match period {
            Period::Min_1 => self
                .round_time(time, trade_sessions)
                .replace_second(0)
                .unwrap(),
            Period::Min_5 | Period::Min_15 | Period::Min_30 => {
                let time = self.round_time(time, trade_sessions);
                let n = period.minutes() as i64;
                let minutes = time.hour() as i64 * 60 + time.minute() as i64 - 1;
                let minutes = (minutes / n + 1) * n;
                let mut time = time.replace_time(
                    Time::from_hms((minutes / 60) as u8, (minutes % 60) as u8, 0).unwrap(),
                );
                for (start, end) in trade_sessions {
                    let s = time.replace_time(*start);
                    if time < s + Duration::minutes(n) {
                        time = s + Duration::minutes(n);
                        break;
                    } else if time <= time.replace_time(*end) {
                        break;
                    }
                }
                time
            }
            Period::Min_60 => {
                let time = self.round_time(time, trade_sessions);
                let (start, end) = trade_sessions
                    .iter()
                    .find(|ts| time.time() >= ts.0 && time.time() <= ts.1)
                    .unwrap();
                let start_minutes = start.hour() as i64 * 60 + start.minute() as i64;
                let curr_minutes = time.hour() as i64 * 60 + time.minute() as i64 - 1;
                let offset_minutes = ((curr_minutes - start_minutes) / 60 + 1) * 60;
                time.replace_time((*start + Duration::minutes(offset_minutes)).min(*end))
            }
            Period::Day => time.replace_time(time!(00:00:00)),
            Period::Week => {
                let week = time.iso_week();
                Date::from_iso_week_date(time.year(), week, Weekday::Monday)
                    .and_then(|date| date.with_hms(0, 0, 0))
                    .unwrap()
                    .assume_utc()
            }
            Period::Month => time
                .replace_day(1)
                .map(|time| time.replace_time(time!(00:00:00)))
                .unwrap(),
            Period::Year => time
                .replace_month(Month::January)
                .and_then(|time| time.replace_day(1))
                .map(|time| time.replace_time(time!(00:00:00)))
                .and_then(|time| time.replace_day(1))
                .unwrap(),
        }
    }

    #[must_use]
    pub fn merge(&self, ty: Type, prev: Option<&Candlestick>, trade: Trade<'_>) -> UpdateAction {
        let Merger { market, .. } = self;
        let tz = market.timezone();
        let time = self.candlestick_time(ty, trade.time.to_timezone(tz));
        let update_fields = market.update_fields(trade.trade_type);

        match prev {
            Some(prev) if time == prev.time => {
                let mut candlestick = *prev;

                if update_fields.contains(UpdateFields::PRICE) {
                    candlestick.high = candlestick.high.max(trade.price);
                    candlestick.low = candlestick.low.min(trade.price);
                    candlestick.close = trade.price;
                }

                if update_fields.contains(UpdateFields::VOLUME) {
                    candlestick.volume += trade.volume;
                    if let Some(volume) = Decimal::from_i64(trade.volume) {
                        candlestick.turnover += trade.price * volume;
                    }
                }

                UpdateAction::UpdateLast(candlestick)
            }
            Some(prev) if time < prev.time => UpdateAction::None,
            _ => {
                if update_fields.contains(UpdateFields::PRICE) {
                    Decimal::from_i64(trade.volume)
                        .map(|volume| {
                            let new_candlestick = Candlestick {
                                time: time.to_timezone(time_tz::timezones::db::UTC),
                                open: trade.price,
                                high: trade.price,
                                low: trade.price,
                                close: trade.price,
                                volume: trade.volume,
                                turnover: trade.price * volume,
                            };
                            UpdateAction::AppendNew(new_candlestick)
                        })
                        .unwrap_or(UpdateAction::None)
                } else {
                    UpdateAction::None
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use time::macros::datetime;

    use super::*;

    #[test]
    fn test_round_time() {
        let trade_sessions = Market::HK.trade_sessions(Type::Normal);
        let merger = Merger::new(Market::HK, Period::Day, false);

        assert_eq!(
            merger.round_time(datetime!(2022-1-1 9:28:0 UTC), trade_sessions),
            datetime!(2022-1-1 9:30:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 9:31:0 UTC), trade_sessions),
            datetime!(2022-1-1 9:31:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 12:0:0 UTC), trade_sessions),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 12:5:0 UTC), trade_sessions),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 13:0:0 UTC), trade_sessions),
            datetime!(2022-1-1 13:0:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 14:0:0 UTC), trade_sessions),
            datetime!(2022-1-1 14:0:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 16:0:0 UTC), trade_sessions),
            datetime!(2022-1-1 16:0:0 UTC)
        );
        assert_eq!(
            merger.round_time(datetime!(2022-1-1 16:2:0 UTC), trade_sessions),
            datetime!(2022-1-1 16:0:0 UTC)
        );
    }

    #[test]
    fn test_time_min1() {
        let merger = Merger::new(Market::HK, Period::Min_1, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 9:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:30:25 UTC)),
            datetime!(2022-1-1 9:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:31:0 UTC)),
            datetime!(2022-1-1 9:31:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 12:05:0 UTC)),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 13:0:0 UTC)),
            datetime!(2022-1-1 13:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:0:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:2:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
    }

    #[test]
    fn test_time_min5() {
        let merger = Merger::new(Market::HK, Period::Min_5, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 9:35:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:30:25 UTC)),
            datetime!(2022-1-1 9:35:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:35:59 UTC)),
            datetime!(2022-1-1 9:35:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:36:0 UTC)),
            datetime!(2022-1-1 9:40:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 12:05:0 UTC)),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 13:0:0 UTC)),
            datetime!(2022-1-1 13:5:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:0:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:2:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
    }

    #[test]
    fn test_time_min15() {
        let merger = Merger::new(Market::HK, Period::Min_15, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 9:45:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:30:25 UTC)),
            datetime!(2022-1-1 9:45:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:35:59 UTC)),
            datetime!(2022-1-1 9:45:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:36:0 UTC)),
            datetime!(2022-1-1 9:45:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 12:05:0 UTC)),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 13:0:0 UTC)),
            datetime!(2022-1-1 13:15:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:0:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:2:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
    }

    #[test]
    fn test_time_min30() {
        let merger = Merger::new(Market::HK, Period::Min_30, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 10:00:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:30:25 UTC)),
            datetime!(2022-1-1 10:00:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:35:59 UTC)),
            datetime!(2022-1-1 10:00:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:36:0 UTC)),
            datetime!(2022-1-1 10:00:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 12:05:0 UTC)),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 13:0:0 UTC)),
            datetime!(2022-1-1 13:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:0:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:2:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
    }

    #[test]
    fn test_time_min60() {
        let merger = Merger::new(Market::HK, Period::Min_60, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:30:25 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:35:59 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:36:0 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 10:30:59 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 10:31:0 UTC)),
            datetime!(2022-1-1 11:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 12:05:0 UTC)),
            datetime!(2022-1-1 12:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 13:0:0 UTC)),
            datetime!(2022-1-1 14:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 14:2:0 UTC)),
            datetime!(2022-1-1 15:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:0:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 16:2:0 UTC)),
            datetime!(2022-1-1 16:0:0 UTC)
        );
    }

    #[test]
    fn test_time_min60_usoq() {
        let merger = Merger::new(Market::US, Period::Min_60, false);

        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 9:30:25 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 9:35:59 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 9:36:0 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 10:30:59 UTC)),
            datetime!(2022-1-1 10:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 10:31:0 UTC)),
            datetime!(2022-1-1 11:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 12:05:0 UTC)),
            datetime!(2022-1-1 12:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 13:0:0 UTC)),
            datetime!(2022-1-1 13:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 14:2:0 UTC)),
            datetime!(2022-1-1 14:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 15:30:59 UTC)),
            datetime!(2022-1-1 15:30:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 15:31:0 UTC)),
            datetime!(2022-1-1 16:15:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::USOQ, datetime!(2022-1-1 16:2:0 UTC)),
            datetime!(2022-1-1 16:15:0 UTC)
        );
    }

    #[test]
    fn test_time_day() {
        let merger = Merger::new(Market::HK, Period::Day, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 9:28:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-1 10:0:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-3 10:0:0 UTC)),
            datetime!(2022-1-3 0:0:0 UTC)
        );
    }

    #[test]
    fn test_time_week() {
        let merger = Merger::new(Market::HK, Period::Week, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-6 9:28:0 UTC)),
            datetime!(2022-1-3 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-10 9:28:0 UTC)),
            datetime!(2022-1-10 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-6-8 9:28:0 UTC)),
            datetime!(2022-6-6 0:0:0 UTC)
        );
    }

    #[test]
    fn test_time_month() {
        let merger = Merger::new(Market::HK, Period::Month, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-6 9:28:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-10 9:28:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-6-8 9:28:0 UTC)),
            datetime!(2022-6-1 0:0:0 UTC)
        );
    }

    #[test]
    fn test_time_year() {
        let merger = Merger::new(Market::HK, Period::Year, false);

        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-1-6 9:28:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-3-10 9:28:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
        assert_eq!(
            merger.candlestick_time(Type::Normal, datetime!(2022-6-8 9:28:0 UTC)),
            datetime!(2022-1-1 0:0:0 UTC)
        );
    }
}