Skip to content

Commit 8f8329f

Browse files
13.0.0: introduce MillisecondPeriod and pass this into TimeRule and AlignedTimeRule to support sample periods smaller than a second. Different logic in both time rules to avoid drift of trigger timestamp
1 parent c3d933d commit 8f8329f

File tree

8 files changed

+130
-86
lines changed

8 files changed

+130
-86
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "trade_aggregation"
3-
version = "12.0.3"
3+
version = "13.0.0"
44
authors = ["MathisWellmann <[email protected]>"]
55
edition = "2021"
66
license-file = "LICENSE"
101 Bytes
Loading

src/aggregation_rules/aligned_time_rule.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{aggregation_rules::TimestampResolution, AggregationRule, ModularCandle, TakerTrade};
1+
use crate::{AggregationRule, MillisecondPeriod, ModularCandle, TakerTrade, TimestampResolution};
22

33
/// The classic time based aggregation rule,
44
/// creating a new candle every n seconds. The time trigger is aligned such that
@@ -13,7 +13,7 @@ pub struct AlignedTimeRule {
1313
// The period for the candle in seconds
1414
// constants can be used nicely here from constants.rs
1515
// e.g.: M1 -> 1 minute candles
16-
period_s: i64,
16+
period_in_units_from_trade: i64,
1717
}
1818

1919
impl AlignedTimeRule {
@@ -24,17 +24,20 @@ impl AlignedTimeRule {
2424
/// period_s: How many seconds a candle will contain
2525
/// ts_res: The resolution each Trade timestamp will have
2626
///
27-
pub fn new(period_s: i64, ts_res: TimestampResolution) -> Self {
28-
let ts_multiplier = match ts_res {
29-
TimestampResolution::Second => 1,
30-
TimestampResolution::Millisecond => 1_000,
31-
TimestampResolution::Microsecond => 1_000_000,
32-
TimestampResolution::Nanosecond => 1_000_000_000,
27+
pub fn new(
28+
period_ms: MillisecondPeriod,
29+
trade_timestamp_resolution: TimestampResolution,
30+
) -> Self {
31+
use TimestampResolution::*;
32+
let ts_multiplier = match trade_timestamp_resolution {
33+
Millisecond => 1,
34+
Microsecond => 1_000,
35+
Nanosecond => 1_000_000,
3336
};
3437

3538
Self {
3639
reference_timestamp: 0,
37-
period_s: period_s * ts_multiplier,
40+
period_in_units_from_trade: period_ms.get() as i64 * ts_multiplier,
3841
}
3942
}
4043

@@ -43,7 +46,7 @@ impl AlignedTimeRule {
4346
/// each period.
4447
#[must_use]
4548
pub fn aligned_timestamp(&self, timestamp: i64) -> i64 {
46-
timestamp - (timestamp % self.period_s)
49+
timestamp - (timestamp % self.period_in_units_from_trade)
4750
}
4851
}
4952

@@ -58,9 +61,14 @@ where
5861
return false;
5962
}
6063

61-
let should_trigger = trade.timestamp() - self.reference_timestamp >= self.period_s;
64+
let should_trigger =
65+
trade.timestamp() - self.reference_timestamp >= self.period_in_units_from_trade;
6266
if should_trigger {
63-
self.reference_timestamp = self.aligned_timestamp(trade.timestamp());
67+
// Advance the trigger timestamp to the next period.
68+
// If the period is too small, then the trade timestamps will out-pace and always trigger.
69+
// Alternatively doing `self.reference_timestamp = self.aligned_timestamp(trade.timestamp())` will cause drift
70+
// and not produce enough samples.
71+
self.reference_timestamp = self.reference_timestamp + self.period_in_units_from_trade
6472
}
6573

6674
should_trigger

src/aggregation_rules/time_rule.rs

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,40 @@
1-
use crate::{AggregationRule, ModularCandle, TakerTrade};
2-
3-
/// The resolution of the "TakerTrade" timestamps
4-
#[derive(Debug, Clone, Copy)]
5-
pub enum TimestampResolution {
6-
/// The timestamp of the TakerTrade is measured in seconds
7-
Second,
8-
9-
/// The timestamp of the TakerTrade is measured in milliseconds
10-
Millisecond,
11-
12-
/// The timestamp of the TakerTrade is measured in microseconds
13-
Microsecond,
14-
15-
/// The timestamp of the TakerTrade is measured in nanoseconds
16-
Nanosecond,
17-
}
1+
use crate::{AggregationRule, MillisecondPeriod, ModularCandle, TakerTrade, TimestampResolution};
182

193
/// The classic time based aggregation rule,
204
/// creating a new candle every n seconds
215
#[derive(Debug, Clone)]
226
pub struct TimeRule {
23-
/// If true, the reference timestamp needs to be reset
24-
init: bool,
25-
267
// The timestamp this rule uses as a reference
8+
// in the unit of the incoming trades.
279
reference_timestamp: i64,
2810

29-
// The period for the candle in seconds
30-
// constants can be used nicely here from constants.rs
31-
// e.g.: M1 -> 1 minute candles
32-
period_s: i64,
11+
// The period for the candle in the timestamp resolution of the candle as provided in the constructor.
12+
period_in_units_from_trade: i64,
3313
}
3414

3515
impl TimeRule {
3616
/// Create a new instance of the time rule,
3717
/// with a given candle period in seconds
3818
///
3919
/// # Arguments:
40-
/// period_s: How many seconds a candle will contain
41-
/// ts_res: The resolution each Trade timestamp will have
20+
/// `period_ms`: How many milliseconds a candle will contain.
21+
/// `trade_timestamp_resolution`: The resolution each Trade timestamp will have
4222
///
43-
pub fn new(period_s: i64, ts_res: TimestampResolution) -> Self {
44-
let ts_multiplier = match ts_res {
45-
TimestampResolution::Second => 1,
46-
TimestampResolution::Millisecond => 1_000,
47-
TimestampResolution::Microsecond => 1_000_000,
48-
TimestampResolution::Nanosecond => 1_000_000_000,
23+
pub fn new(
24+
period_ms: MillisecondPeriod,
25+
trade_timestamp_resolution: TimestampResolution,
26+
) -> Self {
27+
use TimestampResolution::*;
28+
// Given the timestamp resolution of the trades, a certain multiplier is required to compute the number of units for the sample period.
29+
let ts_multiplier = match trade_timestamp_resolution {
30+
Millisecond => 1,
31+
Microsecond => 1_000,
32+
Nanosecond => 1_000_000,
4933
};
5034

5135
Self {
52-
init: true,
5336
reference_timestamp: 0,
54-
period_s: period_s * ts_multiplier,
37+
period_in_units_from_trade: period_ms.get() as i64 * ts_multiplier,
5538
}
5639
}
5740
}
@@ -62,13 +45,17 @@ where
6245
T: TakerTrade,
6346
{
6447
fn should_trigger(&mut self, trade: &T, _candle: &C) -> bool {
65-
if self.init {
48+
if self.reference_timestamp == 0 {
6649
self.reference_timestamp = trade.timestamp();
67-
self.init = false;
6850
}
69-
let should_trigger = trade.timestamp() - self.reference_timestamp > self.period_s;
51+
let should_trigger =
52+
trade.timestamp() - self.reference_timestamp > self.period_in_units_from_trade;
7053
if should_trigger {
71-
self.init = true;
54+
// Advance the trigger timestamp to the next period.
55+
// If the period is too small, then the trade timestamps will out-pace and always trigger.
56+
// Alternatively doing `self.reference_timestamp = trade.timestamp()` will cause drift
57+
// and not produce enough samples.
58+
self.reference_timestamp = self.reference_timestamp + self.period_in_units_from_trade;
7259
}
7360

7461
should_trigger
@@ -107,14 +94,14 @@ mod tests {
10794
false,
10895
);
10996
let candles = aggregate_all_trades(&trades, &mut aggregator);
110-
assert_eq!(candles.len(), 395);
97+
assert_eq!(candles.len(), 396);
11198

11299
let mut aggregator = GenericAggregator::<OhlcCandle, TimeRule, Trade>::new(
113100
TimeRule::new(M5, TimestampResolution::Millisecond),
114101
false,
115102
);
116103
let candles = aggregate_all_trades(&trades, &mut aggregator);
117-
assert_eq!(candles.len(), 1180);
104+
assert_eq!(candles.len(), 1190);
118105

119106
let mut aggregator = GenericAggregator::<OhlcCandle, TimeRule, Trade>::new(
120107
TimeRule::new(H1, TimestampResolution::Millisecond),
@@ -130,14 +117,6 @@ mod tests {
130117
let trades_ms = load_trades_from_csv("data/Bitmex_XBTUSD_1M.csv").unwrap();
131118

132119
// we can therefore transform them into seconds, microseconds and nanoseconds respectively
133-
let trades_s: Vec<Trade> = trades_ms
134-
.iter()
135-
.map(|v| Trade {
136-
timestamp: v.timestamp / 1000,
137-
price: v.price,
138-
size: v.size,
139-
})
140-
.collect();
141120
let trades_micros: Vec<Trade> = trades_ms
142121
.iter()
143122
.map(|v| Trade {
@@ -155,33 +134,26 @@ mod tests {
155134
})
156135
.collect();
157136

158-
// And make sure they produce the same number of candles regardless of timestamp resolution
159-
let mut aggregator = GenericAggregator::<OhlcCandle, TimeRule, Trade>::new(
160-
TimeRule::new(M15, TimestampResolution::Second),
161-
false,
162-
);
163-
let candles = aggregate_all_trades(&trades_s, &mut aggregator);
164-
assert_eq!(candles.len(), 395);
165-
137+
// And make sure they produce the same number of candles given the differing timestamp resolutions.
166138
let mut aggregator = GenericAggregator::<OhlcCandle, TimeRule, Trade>::new(
167139
TimeRule::new(M15, TimestampResolution::Millisecond),
168140
false,
169141
);
170142
let candles = aggregate_all_trades(&trades_ms, &mut aggregator);
171-
assert_eq!(candles.len(), 395);
143+
assert_eq!(candles.len(), 396);
172144

173145
let mut aggregator = GenericAggregator::<OhlcCandle, TimeRule, Trade>::new(
174146
TimeRule::new(M15, TimestampResolution::Microsecond),
175147
false,
176148
);
177149
let candles = aggregate_all_trades(&trades_micros, &mut aggregator);
178-
assert_eq!(candles.len(), 395);
150+
assert_eq!(candles.len(), 396);
179151

180152
let mut aggregator = GenericAggregator::<OhlcCandle, TimeRule, Trade>::new(
181153
TimeRule::new(M15, TimestampResolution::Nanosecond),
182154
false,
183155
);
184156
let candles = aggregate_all_trades(&trades_ns, &mut aggregator);
185-
assert_eq!(candles.len(), 395);
157+
assert_eq!(candles.len(), 396);
186158
}
187159
}

src/aggregator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ mod tests {
133133
candle_counter += 1;
134134
}
135135
}
136-
assert_eq!(candle_counter, 5704);
136+
assert_eq!(candle_counter, 5953);
137137
}
138138

139139
#[test]

src/candle_components/time_velocity.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ impl<T: TakerTrade> CandleComponentUpdate<T> for TimeVelocity {
4343
#[inline(always)]
4444
fn update(&mut self, trade: &T) {
4545
let div = match trade.timestamp_resolution() {
46-
TimestampResolution::Second => 1,
4746
TimestampResolution::Millisecond => 1_000,
4847
TimestampResolution::Microsecond => 1_000_000,
4948
TimestampResolution::Nanosecond => 1_000_000_000,

src/constants.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
1+
use crate::MillisecondPeriod;
2+
13
/// 1 Minute candle period
2-
pub const M1: i64 = 60;
4+
pub const M1: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(60);
35

46
/// 5 Minute candle period
5-
pub const M5: i64 = 300;
7+
pub const M5: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(300);
68

79
/// 15 Minute candle period
8-
pub const M15: i64 = 900;
10+
pub const M15: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(900);
911

1012
/// 30 Minute candle period
11-
pub const M30: i64 = 1800;
13+
pub const M30: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(1800);
1214

1315
/// 1 Hour candle period
14-
pub const H1: i64 = 3600;
16+
pub const H1: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(3600);
1517

1618
/// 2 Hour candle period
17-
pub const H2: i64 = 7200;
19+
pub const H2: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(7200);
1820

1921
/// 4 Hour candle period
20-
pub const H4: i64 = 14400;
22+
pub const H4: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(14400);
2123

2224
/// 8 Hour candle period
23-
pub const H8: i64 = 28800;
25+
pub const H8: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(28800);
2426

2527
/// 12 Hour candle period
26-
pub const H12: i64 = 43200;
28+
pub const H12: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(43200);
2729

2830
/// 1 Day candle period
29-
pub const D1: i64 = 86400;
31+
pub const D1: MillisecondPeriod = MillisecondPeriod::from_non_zero_secs(86400);

src/types.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use crate::TimestampResolution;
2-
31
#[derive(Default, Debug, Clone, Copy, PartialEq)]
42
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53
/// Defines a taker trade
@@ -65,3 +63,68 @@ pub trait TakerTrade {
6563
/// that the trade was a sell order, taking liquidity from the bid.
6664
fn size(&self) -> f64;
6765
}
66+
67+
/// The resolution of the "TakerTrade" timestamps
68+
#[derive(Debug, Clone, Copy)]
69+
pub enum TimestampResolution {
70+
/// The timestamp of the TakerTrade is measured in milliseconds
71+
Millisecond,
72+
73+
/// The timestamp of the TakerTrade is measured in microseconds
74+
Microsecond,
75+
76+
/// The timestamp of the TakerTrade is measured in nanoseconds
77+
Nanosecond,
78+
}
79+
80+
/// A period measured in milliseconds which must be non-zero.
81+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
82+
pub struct MillisecondPeriod(u64);
83+
84+
impl MillisecondPeriod {
85+
/// Try to create the `MillisecondPeriod` from millisecond units.
86+
/// # Panics:
87+
/// If `millis` is zero, the contract was violated.
88+
pub fn from_non_zero(millis: u64) -> Self {
89+
assert!(millis > 0, "`millis` must be non-zero that was the deal");
90+
Self(millis)
91+
}
92+
93+
/// Try to create the `MillisecondPeriod` from seconds.
94+
/// # Panics:
95+
/// Because this is used in a const context, it is not yet possible to do `Option::unwrap` and thus
96+
/// it is asserted that `seconds` is non-zero
97+
pub const fn from_non_zero_secs(seconds: u64) -> Self {
98+
assert!(seconds > 0, "`seconds` must be non-zero that was the deal");
99+
Self(seconds * 1_000)
100+
}
101+
102+
/// Get the inner value
103+
pub fn get(self) -> u64 {
104+
self.0
105+
}
106+
}
107+
108+
#[cfg(test)]
109+
mod test {
110+
use super::*;
111+
112+
#[test]
113+
#[should_panic]
114+
fn millisecond_period_from_non_zero_secs_panic() {
115+
// panics as it cannot be zero
116+
MillisecondPeriod::from_non_zero_secs(0);
117+
}
118+
119+
#[test]
120+
fn millisecond_period_from_non_zero_secs() {
121+
assert_eq!(
122+
MillisecondPeriod::from_non_zero_secs(1),
123+
MillisecondPeriod(1_000)
124+
);
125+
assert_eq!(
126+
MillisecondPeriod::from_non_zero_secs(60),
127+
MillisecondPeriod(60_000)
128+
);
129+
}
130+
}

0 commit comments

Comments
 (0)