|
| 1 | +use quickcheck::Arbitrary; |
| 2 | +use quickcheck_macros::quickcheck; |
| 3 | + |
| 4 | +use crate::time_machine_types::{ |
| 5 | + DataPoint, |
| 6 | + SmaTracker, |
| 7 | + NUM_BUCKETS_THIRTY_MIN, |
| 8 | +}; |
| 9 | + |
| 10 | +#[derive(Clone, Debug, Copy)] |
| 11 | +struct DataEvent { |
| 12 | + time_gap: i64, |
| 13 | + slot_gap: u64, |
| 14 | + price: i64, |
| 15 | +} |
| 16 | + |
| 17 | +impl Arbitrary for DataEvent { |
| 18 | + fn arbitrary(g: &mut quickcheck::Gen) -> Self { |
| 19 | + DataEvent { |
| 20 | + time_gap: i64::from(u8::arbitrary(g)), |
| 21 | + slot_gap: u64::from(u8::arbitrary(g)) + 1, /* Slot gap is always > 1, because there |
| 22 | + * has been a succesful aggregation */ |
| 23 | + price: i64::arbitrary(g), |
| 24 | + } |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +/// This is a generative test for the sma struct. quickcheck will generate a series of |
| 29 | +/// vectors of DataEvents of different length. The generation is based on the arbitrary trait |
| 30 | +/// above. |
| 31 | +/// For each DataEvent : |
| 32 | +/// - time_gap is a random number between 0 and u8::MAX (255) |
| 33 | +/// - slot_gap is a random number between 1 and u8::MAX + 1 (256) |
| 34 | +/// - price is a random i64 |
| 35 | +#[quickcheck] |
| 36 | +fn test_sma(input: Vec<DataEvent>) -> bool { |
| 37 | + // No gaps, no skipped epochs |
| 38 | + let mut tracker1 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero(); |
| 39 | + tracker1.initialize(i64::from(u8::MAX), u64::from(u8::MAX)); |
| 40 | + |
| 41 | + // Skipped and gaps |
| 42 | + let mut tracker2 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero(); |
| 43 | + tracker2.initialize(i64::from(u8::MAX / 5), u64::from(u8::MAX / 5)); |
| 44 | + |
| 45 | + // Gaps, no skips |
| 46 | + let mut tracker3 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero(); |
| 47 | + tracker3.initialize(i64::from(u8::MAX), u64::from(u8::MAX / 5)); |
| 48 | + |
| 49 | + // No skips, gaps |
| 50 | + let mut tracker4 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero(); |
| 51 | + tracker4.initialize(i64::from(u8::MAX), u64::from(u8::MAX / 5) * 4); |
| 52 | + |
| 53 | + // Each epoch is 1 second |
| 54 | + let mut tracker5 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero(); |
| 55 | + tracker5.initialize(1, u64::from(u8::MAX / 5)); |
| 56 | + |
| 57 | + let mut data = Vec::<DataPoint>::new(); |
| 58 | + |
| 59 | + let mut current_time = 0i64; |
| 60 | + for data_event in input.clone() { |
| 61 | + let datapoint = DataPoint { |
| 62 | + previous_timestamp: current_time, |
| 63 | + current_timestamp: current_time + data_event.time_gap, |
| 64 | + slot_gap: data_event.slot_gap, |
| 65 | + price: data_event.price, |
| 66 | + }; |
| 67 | + |
| 68 | + tracker1.add_datapoint(&datapoint).unwrap(); |
| 69 | + tracker2.add_datapoint(&datapoint).unwrap(); |
| 70 | + tracker3.add_datapoint(&datapoint).unwrap(); |
| 71 | + tracker4.add_datapoint(&datapoint).unwrap(); |
| 72 | + tracker5.add_datapoint(&datapoint).unwrap(); |
| 73 | + data.push(datapoint); |
| 74 | + current_time += data_event.time_gap; |
| 75 | + |
| 76 | + tracker1.check_current_epoch_fields(&data, current_time); |
| 77 | + tracker2.check_current_epoch_fields(&data, current_time); |
| 78 | + tracker3.check_current_epoch_fields(&data, current_time); |
| 79 | + tracker4.check_current_epoch_fields(&data, current_time); |
| 80 | + tracker5.check_current_epoch_fields(&data, current_time); |
| 81 | + tracker1.check_array_fields(&data, current_time); |
| 82 | + tracker2.check_array_fields(&data, current_time); |
| 83 | + tracker3.check_array_fields(&data, current_time); |
| 84 | + tracker4.check_array_fields(&data, current_time); |
| 85 | + tracker5.check_array_fields(&data, current_time); |
| 86 | + } |
| 87 | + |
| 88 | + return true; |
| 89 | +} |
| 90 | + |
| 91 | + |
| 92 | +impl<const NUM_ENTRIES: usize> SmaTracker<NUM_ENTRIES> { |
| 93 | + pub fn zero() -> Self { |
| 94 | + return SmaTracker::<NUM_ENTRIES> { |
| 95 | + granularity: 0, |
| 96 | + threshold: 0, |
| 97 | + current_epoch_denominator: 0, |
| 98 | + current_epoch_is_valid: false, |
| 99 | + current_epoch_numerator: 0, |
| 100 | + running_valid_epoch_counter: [0u64; NUM_ENTRIES], |
| 101 | + running_sum_of_price_averages: [0i128; NUM_ENTRIES], |
| 102 | + }; |
| 103 | + } |
| 104 | + |
| 105 | + pub fn check_current_epoch_fields(&self, data: &Vec<DataPoint>, time: i64) { |
| 106 | + let curent_epoch = self.time_to_epoch(time).unwrap(); |
| 107 | + |
| 108 | + let result = self.compute_epoch_expected_values(data, curent_epoch); |
| 109 | + assert_eq!(self.current_epoch_denominator, result.0); |
| 110 | + assert_eq!(self.current_epoch_numerator, result.1); |
| 111 | + assert_eq!(self.current_epoch_is_valid, result.2); |
| 112 | + } |
| 113 | + |
| 114 | + pub fn check_array_fields(&self, data: &Vec<DataPoint>, time: i64) { |
| 115 | + let current_epoch = self.time_to_epoch(time).unwrap(); |
| 116 | + let mut values = vec![]; |
| 117 | + |
| 118 | + // Compute all epoch averages |
| 119 | + for i in 0..current_epoch { |
| 120 | + values.push(self.compute_epoch_expected_values(data, i)); |
| 121 | + } |
| 122 | + |
| 123 | + // Get running sums |
| 124 | + let running_sum_price_iter = values.iter().scan((0, 0), |res, &y| { |
| 125 | + res.0 = res.0 + y.1 / i128::from(y.0); |
| 126 | + res.1 = res.1 + u64::from(y.2); |
| 127 | + Some(*res) |
| 128 | + }); |
| 129 | + |
| 130 | + // Compare to running_sum_of_price_averages |
| 131 | + let mut i = (current_epoch + NUM_ENTRIES - 1).rem_euclid(NUM_ENTRIES); |
| 132 | + for x in running_sum_price_iter |
| 133 | + .collect::<Vec<(i128, u64)>>() |
| 134 | + .iter() |
| 135 | + .rev() |
| 136 | + .take(NUM_ENTRIES) |
| 137 | + { |
| 138 | + assert_eq!(self.running_sum_of_price_averages[i], x.0); |
| 139 | + assert_eq!(self.running_valid_epoch_counter[i], x.1); |
| 140 | + i = (i + NUM_ENTRIES - 1).rem_euclid(NUM_ENTRIES); |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + pub fn compute_epoch_expected_values( |
| 145 | + &self, |
| 146 | + data: &Vec<DataPoint>, |
| 147 | + epoch_number: usize, |
| 148 | + ) -> (u64, i128, bool) { |
| 149 | + let left_bound = self |
| 150 | + .granularity |
| 151 | + .checked_mul(epoch_number.try_into().unwrap()) |
| 152 | + .unwrap(); |
| 153 | + |
| 154 | + let right_bound = self |
| 155 | + .granularity |
| 156 | + .checked_mul((epoch_number + 1).try_into().unwrap()) |
| 157 | + .unwrap(); |
| 158 | + |
| 159 | + |
| 160 | + let mut result = data.iter().fold((0, 0, true), |x: (u64, i128, bool), y| { |
| 161 | + if !((left_bound > y.current_timestamp) || (right_bound <= y.previous_timestamp)) |
| 162 | + //Check interval intersection |
| 163 | + { |
| 164 | + let is_valid = y.slot_gap <= self.threshold; |
| 165 | + return ( |
| 166 | + x.0 + y.slot_gap, |
| 167 | + x.1 + i128::from(y.slot_gap) * i128::from(y.price), |
| 168 | + x.2 && is_valid, |
| 169 | + ); |
| 170 | + } |
| 171 | + return x; |
| 172 | + }); |
| 173 | + |
| 174 | + if epoch_number == 0 { |
| 175 | + result.2 = false; |
| 176 | + } |
| 177 | + return result; |
| 178 | + } |
| 179 | +} |
0 commit comments