fix(core): skip non-positive HV prices and add Error::InvalidTick (R13, R14)
R13 — `HistoricalVolatility::update` previously substituted `0.0` for
the log-return whenever `prev <= 0` or `input <= 0`. The log-return is
undefined there, and silently treating bad ticks as "no movement"
underreports realised volatility on broken data feeds. The fix skips
non-positive prices entirely: `self.last` is returned, state is left
untouched, and the next real tick re-anchors against the previous
*valid* `prev_price`. This matches how every other indicator handles
invalid inputs (SMA / EMA / ROC / Bollinger).
A new test `skips_non_positive_prices` proves the invariant: after a
warmed-up indicator, two consecutive bad ticks (`-5.0` and `0.0`) must
return the baseline value, and a subsequent real positive tick must
produce the same output as a control indicator that simply never saw
the bad ticks.
R14 — `Tick::new` previously returned `Error::InvalidCandle` for
negative volume. A tick is not a candle; downstream tick-stream
pipelines should be able to match on a semantically-correct error. A
new `Error::InvalidTick { message }` variant is added; the existing
test is updated to assert against it. Python's `map_err` is extended
to forward the new variant as `PyValueError`; the Node and WASM
bindings format via `Error::to_string()` and pick the new variant up
automatically without source changes.
This commit is contained in:
@@ -152,13 +152,14 @@ impl Tick {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::NonFiniteInput`] if `price` or `volume` is NaN or infinite,
|
||||
/// or [`Error::InvalidCandle`] for `volume < 0`.
|
||||
/// or [`Error::InvalidTick`] for `volume < 0`. (Audit finding R14 — previously
|
||||
/// returned [`Error::InvalidCandle`], which is semantically wrong for a tick.)
|
||||
pub fn new(price: f64, volume: f64, timestamp: i64) -> Result<Self> {
|
||||
if !price.is_finite() || !volume.is_finite() {
|
||||
return Err(Error::NonFiniteInput);
|
||||
}
|
||||
if volume < 0.0 {
|
||||
return Err(Error::InvalidCandle {
|
||||
return Err(Error::InvalidTick {
|
||||
message: "tick volume must be non-negative",
|
||||
});
|
||||
}
|
||||
@@ -279,7 +280,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tick_new_rejects_negative_volume() {
|
||||
// Audit R14: the variant is `InvalidTick`, not `InvalidCandle` — a tick
|
||||
// is not a candle, and downstream pipelines should be able to match on
|
||||
// the correct semantic.
|
||||
let err = Tick::new(100.0, -1.0, 0).unwrap_err();
|
||||
assert!(matches!(err, Error::InvalidCandle { .. }));
|
||||
assert!(matches!(err, Error::InvalidTick { .. }));
|
||||
assert!(
|
||||
err.to_string().contains("tick volume"),
|
||||
"expected the InvalidTick message in the formatted error, got {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user