1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::aggregation_rules::TimestampResolution;
use crate::{AggregationRule, ModularCandle, TakerTrade};

/// The classic time based aggregation rule,
/// creating a new candle every n seconds.  The time trigger is aligned such that
/// the trigger points are starting from a time equals zero.  For example, if the first
/// tick comes in a 1:32:00 on a 5 minute candle, that first candle will only contain
/// 3 minutes of trades, representing a 1:30 start.
pub struct AlignedTimeRule {
    /// If true, the reference timestamp needs to be reset
    init: bool,

    // The timestamp this rule uses as a reference
    reference_timestamp: i64,

    // The period for the candle in seconds
    // constants can be used nicely here from constants.rs
    // e.g.: M1 -> 1 minute candles
    period_s: i64,
}

impl AlignedTimeRule {
    /// Create a new instance of the aligned time rule,
    /// with a given candle period in seconds
    ///
    /// # Arguments:
    /// period_s: How many seconds a candle will contain
    /// ts_res: The resolution each Trade timestamp will have
    ///
    pub fn new(period_s: i64, ts_res: TimestampResolution) -> Self {
        let ts_multiplier = match ts_res {
            TimestampResolution::Second => 1,
            TimestampResolution::Millisecond => 1_000,
            TimestampResolution::Microsecond => 1_000_000,
            TimestampResolution::Nanosecond => 1_000_000_000,
        };

        Self {
            init: true,
            reference_timestamp: 0,
            period_s: period_s * ts_multiplier,
        }
    }

    /// Calculates the "aligned" timestamp, which the rule will use when receiving
    /// for determining the trigger.  This is done at the initialization of
    /// each period.
    #[must_use]
    pub fn aligned_timestamp(&self, timestamp: i64) -> i64 {
        timestamp - (timestamp % self.period_s)
    }
}

impl<C, T> AggregationRule<C, T> for AlignedTimeRule
where
    C: ModularCandle<T>,
    T: TakerTrade,
{
    fn should_trigger(&mut self, trade: &T, _candle: &C) -> bool {
        if self.init {
            self.reference_timestamp = self.aligned_timestamp(trade.timestamp());
            self.init = false;
        }
        let should_trigger = trade.timestamp() - self.reference_timestamp > self.period_s;
        if should_trigger {
            self.init = true;
        }

        should_trigger
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        aggregate_all_trades,
        candle_components::{CandleComponent, CandleComponentUpdate, Close, High, Low, Open},
        load_trades_from_csv, GenericAggregator, ModularCandle, Trade, M15,
    };
    use trade_aggregation_derive::Candle;

    use super::*;

    #[test]
    fn aligned_time_rule() {
        #[derive(Debug, Default, Clone, Candle)]
        struct AlignedCandle {
            pub open: Open,
            high: High,
            low: Low,
            pub close: Close,
        }

        let trades = load_trades_from_csv("data/Bitmex_XBTUSD_1M.csv").unwrap();

        let mut aggregator = GenericAggregator::<AlignedCandle, AlignedTimeRule, Trade>::new(
            AlignedTimeRule::new(M15, TimestampResolution::Millisecond),
        );
        let candles = aggregate_all_trades(&trades, &mut aggregator);
        assert_eq!(candles.len(), 396);

        // make sure that the aggregator starts a new candle with the "trigger tick"
        // rather than updating the existing candle with the "trigger tick"
        let c = &candles[0];
        assert_eq!(c.open.value(), 13873.0);
        assert_eq!(c.close.value(), 13769.0);
        let c = &candles[1];
        assert_eq!(c.open.value(), 13768.5);
        assert_eq!(c.close.value(), 13721.5);
    }
}