Skip to content

Commit

Permalink
Version 8 release. Includes the information of the trade that trigger…
Browse files Browse the repository at this point in the history
…s the new candle both in the old and the new one, to avoid degenerate candles when using the `RelativePriceRule`
  • Loading branch information
MathisWellmann committed Apr 29, 2023
1 parent 7997155 commit ff766c0
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "trade_aggregation"
version = "7.2.1"
version = "8.0.0"
authors = ["MathisWellmann <wellmannmathis@gmail.com>"]
edition = "2021"
license-file = "LICENSE"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified img/time_candles_plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 8 additions & 20 deletions src/aggregation_rules/aligned_time_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<AlignedCandle, AlignedTimeRule, Trade>::new(
let mut aggregator = GenericAggregator::<OhlcCandle, 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"
// 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);
}
}
55 changes: 47 additions & 8 deletions src/aggregation_rules/relative_price_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
if threshold_delta <= 0.0 {
pub fn new(threshold_fraction: f64) -> Result<Self> {
if threshold_fraction <= 0.0 {
return Err(Error::InvalidParam);
}
Ok(Self {
init: true,
init_price: 0.0,
threshold_delta,
threshold_fraction,
})
}
}
Expand All @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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::<OhlcCandle, _, Trade>::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::<OhlcCandle, _, Trade>::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();
}
}
1 change: 1 addition & 0 deletions src/aggregation_rules/time_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
14 changes: 10 additions & 4 deletions src/aggregator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,21 @@ where
T: TakerTrade,
{
fn update(&mut self, trade: &T) -> Option<C> {
// 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
}
}

Expand Down

0 comments on commit ff766c0

Please sign in to comment.