diff --git a/Cargo.toml b/Cargo.toml index 238f3d6..94b4875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trade_aggregation" -version = "7.2.1" +version = "8.0.0" authors = ["MathisWellmann "] edition = "2021" license-file = "LICENSE" diff --git a/img/relative_price_candles_plot.png b/img/relative_price_candles_plot.png new file mode 100644 index 0000000..e69de29 diff --git a/img/time_candles_plot.png b/img/time_candles_plot.png index cb23acc..d50cced 100644 Binary files a/img/time_candles_plot.png and b/img/time_candles_plot.png differ diff --git a/src/aggregation_rules/aligned_time_rule.rs b/src/aggregation_rules/aligned_time_rule.rs index 7223812..f9e2e2f 100644 --- a/src/aggregation_rules/aligned_time_rule.rs +++ b/src/aggregation_rules/aligned_time_rule.rs @@ -71,40 +71,28 @@ where #[cfg(test)] mod tests { - use trade_aggregation_derive::Candle; - use super::*; use crate::{ - aggregate_all_trades, - candle_components::{CandleComponent, CandleComponentUpdate, Close, High, Low, Open}, - load_trades_from_csv, GenericAggregator, ModularCandle, Trade, M15, + aggregate_all_trades, load_trades_from_csv, plot::OhlcCandle, GenericAggregator, Trade, M15, }; #[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::::new( + let mut aggregator = GenericAggregator::::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" + // make sure that the aggregator starts a new candle with the "trigger tick", + // and includes that information of the trade that triggered the new candle as well let c = &candles[0]; - assert_eq!(c.open.value(), 13873.0); - assert_eq!(c.close.value(), 13769.0); + assert_eq!(c.open(), 13873.0); + assert_eq!(c.close(), 13768.5); let c = &candles[1]; - assert_eq!(c.open.value(), 13768.5); - assert_eq!(c.close.value(), 13721.5); + assert_eq!(c.open(), 13768.5); + assert_eq!(c.close(), 13722.0); } } diff --git a/src/aggregation_rules/relative_price_rule.rs b/src/aggregation_rules/relative_price_rule.rs index 12d7aff..34b3a22 100644 --- a/src/aggregation_rules/relative_price_rule.rs +++ b/src/aggregation_rules/relative_price_rule.rs @@ -4,23 +4,23 @@ use crate::{AggregationRule, Error, ModularCandle, Result, TakerTrade}; pub struct RelativePriceRule { init: bool, init_price: f64, - threshold_delta: f64, + threshold_fraction: f64, } impl RelativePriceRule { /// Create a new instance. /// /// # Arguments: - /// `threshold_delta`: The trigger condition + /// `threshold_fraction`: The relative distance ((p_t - p_i) / p_i) the price needs to move before a new candle creation is triggered. /// - pub fn new(threshold_delta: f64) -> Result { - if threshold_delta <= 0.0 { + pub fn new(threshold_fraction: f64) -> Result { + if threshold_fraction <= 0.0 { return Err(Error::InvalidParam); } Ok(Self { init: true, init_price: 0.0, - threshold_delta, + threshold_fraction, }) } } @@ -39,8 +39,8 @@ where let price_delta = (trade.price() - self.init_price).abs() / self.init_price; - if price_delta >= self.threshold_delta { - self.init = true; + if price_delta >= self.threshold_fraction { + self.init_price = trade.price(); return true; } false @@ -50,7 +50,11 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{plot::OhlcCandle, Trade}; + use crate::{ + aggregate_all_trades, load_trades_from_csv, + plot::{plot_ohlc_candles, OhlcCandle}, + GenericAggregator, Trade, + }; #[test] fn relative_price_rule() { @@ -112,4 +116,39 @@ mod tests { true ); } + + #[test] + fn relative_price_rule_real_data() { + let trades = load_trades_from_csv("./data/Bitmex_XBTUSD_1M.csv").expect("Unable to load trades at this path, are you sure you're in the root directory of the project?"); + + // 0.5% candles + const THRESHOLD: f64 = 0.005; + let rule = RelativePriceRule::new(0.01).unwrap(); + let mut aggregator = GenericAggregator::::new(rule); + let candles = aggregate_all_trades(&trades, &mut aggregator); + assert!(!candles.is_empty()); + + for c in candles { + assert!((c.high() - c.low()) / c.low() >= THRESHOLD); + assert!((c.close() - c.open()).abs() / c.open() >= THRESHOLD); + } + } + + #[test] + fn relative_price_candles_plot() { + let trades = load_trades_from_csv("data/Bitmex_XBTUSD_1M.csv").unwrap(); + + const THRESHOLD: f64 = 0.005; + let rule = RelativePriceRule::new(THRESHOLD).unwrap(); + let mut aggregator = GenericAggregator::::new(rule); + let candles = aggregate_all_trades(&trades, &mut aggregator); + println!("got {} candles", candles.len()); + + plot_ohlc_candles( + &candles, + "img/relative_price_candles_plot.png", + (3840, 2160), + ) + .unwrap(); + } } diff --git a/src/aggregation_rules/time_rule.rs b/src/aggregation_rules/time_rule.rs index 5ed5e38..082c617 100644 --- a/src/aggregation_rules/time_rule.rs +++ b/src/aggregation_rules/time_rule.rs @@ -97,6 +97,7 @@ mod tests { plot_ohlc_candles(&candles, "img/time_candles_plot.png", (2560, 1440)).unwrap(); } + #[test] fn time_rule_differing_periods() { let trades = load_trades_from_csv("data/Bitmex_XBTUSD_1M.csv").unwrap(); diff --git a/src/aggregator.rs b/src/aggregator.rs index 6a645b5..24742e5 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -49,15 +49,21 @@ where T: TakerTrade, { fn update(&mut self, trade: &T) -> Option { + // Always update the candle with the newest information. + self.candle.update(trade); + if self.aggregation_rule.should_trigger(trade, &self.candle) { let candle = self.candle.clone(); self.candle.reset(); + // Also include the initial information in the candle. + // This means the trade data at the boundary is included twice. + // This especially ensures `Open` and `Close` values are correct. + // TODO: If this behaviour of including trade info at the boundary in both candles, + // A flag may be added to specify the exact behaviour. self.candle.update(trade); - Some(candle) - } else { - self.candle.update(trade); - None + return Some(candle); } + None } }