C5: reject out-of-order candles in the resampler
Resampler::push previously closed the open bar and opened a new one for any candle whose bucket differed from the open bar, including buckets strictly before it — silently corrupting the output for out-of-order input. TickAggregator::push already rejects this case with an error. Change push to return Result<Option<Candle>>: candles in an earlier bucket than the open bar now yield Error::Malformed, matching the aggregator. resample_all propagates the error via `?`. The doc comment keeps the input/output multiple relationship as a documented caller responsibility, since Resampler does not know the input timeframe.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
//! Resample an existing candle stream from a finer timeframe to a coarser one.
|
//! Resample an existing candle stream from a finer timeframe to a coarser one.
|
||||||
|
|
||||||
use crate::aggregator::Timeframe;
|
use crate::aggregator::Timeframe;
|
||||||
use crate::error::Result;
|
use crate::error::{Error, Result};
|
||||||
use wickra_core::Candle;
|
use wickra_core::Candle;
|
||||||
|
|
||||||
/// Roll a stream of candles up to a coarser timeframe.
|
/// Roll a stream of candles up to a coarser timeframe.
|
||||||
@@ -72,22 +72,31 @@ impl Resampler {
|
|||||||
|
|
||||||
/// Push a finer-grained candle. Returns the coarser candle that just closed,
|
/// Push a finer-grained candle. Returns the coarser candle that just closed,
|
||||||
/// if any.
|
/// if any.
|
||||||
pub fn push(&mut self, candle: Candle) -> Option<Candle> {
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns [`Error::Malformed`] if `candle.timestamp` falls into a bucket
|
||||||
|
/// strictly before the currently open bar — out-of-order candles are not
|
||||||
|
/// supported, matching [`crate::aggregator::TickAggregator::push`].
|
||||||
|
pub fn push(&mut self, candle: Candle) -> Result<Option<Candle>> {
|
||||||
let bucket = self.timeframe.floor(candle.timestamp);
|
let bucket = self.timeframe.floor(candle.timestamp);
|
||||||
match self.open {
|
match self.open {
|
||||||
Some(mut bar) if bucket == bar.bucket_start => {
|
Some(mut bar) if bucket == bar.bucket_start => {
|
||||||
bar.absorb(candle);
|
bar.absorb(candle);
|
||||||
self.open = Some(bar);
|
self.open = Some(bar);
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
Some(bar) => {
|
Some(bar) if bucket > bar.bucket_start => {
|
||||||
let closed = bar.into_candle();
|
let closed = bar.into_candle();
|
||||||
self.open = Some(RolledBar::from_candle(candle, bucket));
|
self.open = Some(RolledBar::from_candle(candle, bucket));
|
||||||
Some(closed)
|
Ok(Some(closed))
|
||||||
}
|
}
|
||||||
|
Some(bar) => Err(Error::Malformed(format!(
|
||||||
|
"candle timestamp {} is older than the open bar start {}",
|
||||||
|
candle.timestamp, bar.bucket_start
|
||||||
|
))),
|
||||||
None => {
|
None => {
|
||||||
self.open = Some(RolledBar::from_candle(candle, bucket));
|
self.open = Some(RolledBar::from_candle(candle, bucket));
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +117,7 @@ where
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for c in iter {
|
for c in iter {
|
||||||
let c = c?;
|
let c = c?;
|
||||||
if let Some(closed) = r.push(c) {
|
if let Some(closed) = r.push(c)? {
|
||||||
out.push(closed);
|
out.push(closed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,4 +159,23 @@ mod tests {
|
|||||||
assert_eq!(b.open, 11.5);
|
assert_eq!(b.open, 11.5);
|
||||||
assert_eq!(b.timestamp, 5);
|
assert_eq!(b.timestamp, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_out_of_order_candle() {
|
||||||
|
let mut r = Resampler::new(Timeframe::new(5).unwrap());
|
||||||
|
assert!(r.push(c(10, 10.0, 11.0, 9.0, 10.5, 1.0)).unwrap().is_none());
|
||||||
|
// A candle in an earlier bucket than the open bar is rejected.
|
||||||
|
let err = r.push(c(2, 10.0, 11.0, 9.0, 10.5, 1.0)).unwrap_err();
|
||||||
|
assert!(matches!(err, Error::Malformed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_bucket_candles_aggregate() {
|
||||||
|
let mut r = Resampler::new(Timeframe::new(5).unwrap());
|
||||||
|
assert!(r.push(c(0, 10.0, 11.0, 9.0, 10.5, 1.0)).unwrap().is_none());
|
||||||
|
assert!(r.push(c(3, 10.5, 12.0, 10.0, 11.0, 1.0)).unwrap().is_none());
|
||||||
|
let bar = r.flush().unwrap();
|
||||||
|
assert_eq!(bar.high, 12.0);
|
||||||
|
assert_eq!(bar.low, 9.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user