diff --git a/CHANGELOG.md b/CHANGELOG.md
index f98ccfc0..a1fdea15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Depth Slope** — the mean per-side OLS slope of cumulative resting size
against distance from the mid, measuring how fast the book thickens away
from the touch.
+- **Microstructure family — footprint (part 4).** **Footprint** decomposes the
+ volume traded in a bar across price buckets (`round(price / tick_size)`),
+ splitting each bucket into buy-initiated (ask) and sell-initiated (bid)
+ volume. A multi-output, variable-length indicator: every `update` returns the
+ full footprint accumulated since the last `reset`, exposed in Rust, Python
+ (`(k, 3)` arrays), Node (`{ price, bidVol, askVol }` rows) and WASM.
## [0.4.2] - 2026-06-01
diff --git a/README.md b/README.md
index 1b73e625..2f61d4f3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
- every one of the 231 indicators; start at the
+ every one of the 232 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
## Indicators
-231 streaming-first indicators across seventeen families. Every one passes the
+232 streaming-first indicators across seventeen families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
@@ -156,7 +156,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down |
-| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda |
+| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
-│ ├── wickra-core/ core engine + all 231 indicators
+│ ├── wickra-core/ core engine + all 232 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
├── bindings/
diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js
index 00159e75..c4e27c3b 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -1063,3 +1063,34 @@ test('price-impact rejects bad input', () => {
assert.throws(() => new wickra.RealizedSpread(0));
assert.throws(() => new wickra.KylesLambda(1));
});
+
+test('footprint buckets buy and sell volume per price level', () => {
+ const fp = new wickra.Footprint(1.0);
+ fp.update(100.2, 2, true); // bucket 100 -> ask 2
+ fp.update(100.7, 3, false); // bucket 101 -> bid 3
+ const out = fp.update(100.1, 1, true); // bucket 100 -> ask 3
+ assert.equal(out.length, 2);
+ assert.deepEqual(
+ { price: out[0].price, bidVol: out[0].bidVol, askVol: out[0].askVol },
+ { price: 100.0, bidVol: 0.0, askVol: 3.0 },
+ );
+ assert.deepEqual(
+ { price: out[1].price, bidVol: out[1].bidVol, askVol: out[1].askVol },
+ { price: 101.0, bidVol: 3.0, askVol: 0.0 },
+ );
+});
+
+test('footprint streaming update matches batch and rejects bad tick', () => {
+ const n = 12;
+ const price = Array.from({ length: n }, (_, i) => 100 + (i % 5) * 0.3);
+ const size = Array.from({ length: n }, (_, i) => 1 + (i % 3));
+ const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0);
+ const batch = new wickra.Footprint(1.0).batch(price, size, isBuy);
+ const streamer = new wickra.Footprint(1.0);
+ assert.equal(batch.length, n);
+ for (let i = 0; i < n; i++) {
+ const s = streamer.update(price[i], size[i], isBuy[i]);
+ assert.deepEqual(s, batch[i], `mismatch at ${i}`);
+ }
+ assert.throws(() => new wickra.Footprint(0));
+});
diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts
index 087cc1bb..0be49796 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -286,6 +286,12 @@ export interface ObSnapshot {
askPx: Array
askSz: Array
}
+/** One price bucket of a footprint. */
+export interface FootprintLevelValue {
+ price: number
+ bidVol: number
+ askVol: number
+}
export type SmaNode = SMA
export declare class SMA {
constructor(period: number)
@@ -2304,6 +2310,15 @@ export declare class KylesLambda {
isReady(): boolean
warmupPeriod(): number
}
+export type FootprintNode = Footprint
+export declare class Footprint {
+ constructor(tickSize: number)
+ update(price: number, size: number, isBuy: boolean): Array
+ batch(price: Array, size: Array, isBuy: Array): Array>
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type SharpeRatioNode = SharpeRatio
export declare class SharpeRatio {
constructor(period: number, riskFree: number)
diff --git a/bindings/node/index.js b/bindings/node/index.js
index 1136071a..5840116e 100644
--- a/bindings/node/index.js
+++ b/bindings/node/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
-const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
+const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
module.exports.version = version
module.exports.SMA = SMA
@@ -527,6 +527,7 @@ module.exports.TradeImbalance = TradeImbalance
module.exports.EffectiveSpread = EffectiveSpread
module.exports.RealizedSpread = RealizedSpread
module.exports.KylesLambda = KylesLambda
+module.exports.Footprint = Footprint
module.exports.SharpeRatio = SharpeRatio
module.exports.SortinoRatio = SortinoRatio
module.exports.CalmarRatio = CalmarRatio
diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs
index 8c895525..23ccfa61 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -9264,6 +9264,94 @@ impl KylesLambdaNode {
}
}
+// ============================== Microstructure: Footprint ==============================
+//
+// Footprint is a multi-output, variable-length indicator. Each `update(price,
+// size, isBuy)` returns the full bar footprint accumulated since the last
+// `reset()` as an array of `{ price, bidVol, askVol }` rows (sorted ascending
+// by price); `batch` returns an array of such arrays, one per trade.
+
+/// One price bucket of a footprint.
+#[napi(object)]
+pub struct FootprintLevelValue {
+ pub price: f64,
+ pub bid_vol: f64,
+ pub ask_vol: f64,
+}
+
+fn footprint_levels(out: &wc::FootprintOutput) -> Vec {
+ out.levels
+ .iter()
+ .map(|level| FootprintLevelValue {
+ price: level.price,
+ bid_vol: level.bid_vol,
+ ask_vol: level.ask_vol,
+ })
+ .collect()
+}
+
+#[napi(js_name = "Footprint")]
+pub struct FootprintNode {
+ inner: wc::Footprint,
+}
+
+#[napi]
+impl FootprintNode {
+ #[napi(constructor)]
+ pub fn new(tick_size: f64) -> napi::Result {
+ Ok(Self {
+ inner: wc::Footprint::new(tick_size).map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(
+ &mut self,
+ price: f64,
+ size: f64,
+ is_buy: bool,
+ ) -> napi::Result> {
+ let out = self
+ .inner
+ .update(build_trade(price, size, is_buy)?)
+ .expect("footprint emits on every trade");
+ Ok(footprint_levels(&out))
+ }
+ #[napi]
+ pub fn batch(
+ &mut self,
+ price: Vec,
+ size: Vec,
+ is_buy: Vec,
+ ) -> napi::Result>> {
+ if price.len() != size.len() || size.len() != is_buy.len() {
+ return Err(NapiError::from_reason(
+ "price, size, is_buy must be equal length".to_string(),
+ ));
+ }
+ let mut out = Vec::with_capacity(price.len());
+ for i in 0..price.len() {
+ let snapshot = self
+ .inner
+ .update(build_trade(price[i], size[i], is_buy[i])?)
+ .expect("footprint emits on every trade");
+ out.push(footprint_levels(&snapshot));
+ }
+ Ok(out)
+ }
+ #[napi]
+ pub fn reset(&mut self) {
+ self.inner.reset();
+ }
+ #[napi(js_name = "isReady")]
+ pub fn is_ready(&self) -> bool {
+ self.inner.is_ready()
+ }
+ #[napi(js_name = "warmupPeriod")]
+ pub fn warmup_period(&self) -> u32 {
+ self.inner.warmup_period() as u32
+ }
+}
+
// ============================== Family 15: Risk / Performance ==============================
// Risk metrics with fallible `new` (most need `period >= 2`), so each wrapper
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index b3e97e26..3ed6c433 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -255,6 +255,8 @@ from ._wickra import (
EffectiveSpread,
RealizedSpread,
KylesLambda,
+ # Microstructure: footprint
+ Footprint,
# Risk / Performance
SharpeRatio,
SortinoRatio,
@@ -507,6 +509,8 @@ __all__ = [
"EffectiveSpread",
"RealizedSpread",
"KylesLambda",
+ # Microstructure: footprint
+ "Footprint",
# Risk / Performance
"SharpeRatio",
"SortinoRatio",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 074bdd03..9f763f00 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -12095,6 +12095,93 @@ impl PyKylesLambda {
}
}
+// ============================== Microstructure: Footprint ==============================
+//
+// Footprint is a multi-output, variable-length indicator: each `update(price,
+// size, is_buy)` returns the full bar footprint accumulated since the last
+// `reset()` as a `(k, 3)` array with columns `[price, bid_vol, ask_vol]`, one
+// row per touched price bucket (sorted ascending by price). `batch` returns a
+// list of such arrays, one per trade.
+
+fn footprint_to_array<'py>(
+ py: Python<'py>,
+ out: &wc::FootprintOutput,
+) -> Bound<'py, PyArray2> {
+ let rows = out.levels.len();
+ let mut data = Vec::with_capacity(rows * 3);
+ for level in &out.levels {
+ data.push(level.price);
+ data.push(level.bid_vol);
+ data.push(level.ask_vol);
+ }
+ numpy::ndarray::Array2::from_shape_vec((rows, 3), data)
+ .expect("shape consistent")
+ .into_pyarray(py)
+}
+
+#[pyclass(name = "Footprint", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyFootprint {
+ inner: wc::Footprint,
+}
+
+#[pymethods]
+impl PyFootprint {
+ #[new]
+ fn new(tick_size: f64) -> PyResult {
+ Ok(Self {
+ inner: wc::Footprint::new(tick_size).map_err(map_err)?,
+ })
+ }
+ fn update<'py>(
+ &mut self,
+ py: Python<'py>,
+ price: f64,
+ size: f64,
+ is_buy: bool,
+ ) -> PyResult>> {
+ let out = self
+ .inner
+ .update(build_trade(price, size, is_buy)?)
+ .expect("footprint emits on every trade");
+ Ok(footprint_to_array(py, &out))
+ }
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ price: Vec,
+ size: Vec,
+ is_buy: Vec,
+ ) -> PyResult>>> {
+ if price.len() != size.len() || size.len() != is_buy.len() {
+ return Err(PyValueError::new_err(
+ "price, size, is_buy must be equal length",
+ ));
+ }
+ let mut out = Vec::with_capacity(price.len());
+ for i in 0..price.len() {
+ let snapshot = self
+ .inner
+ .update(build_trade(price[i], size[i], is_buy[i])?)
+ .expect("footprint emits on every trade");
+ out.push(footprint_to_array(py, &snapshot));
+ }
+ Ok(out)
+ }
+ fn reset(&mut self) {
+ self.inner.reset();
+ }
+ fn is_ready(&self) -> bool {
+ self.inner.is_ready()
+ }
+ fn warmup_period(&self) -> usize {
+ self.inner.warmup_period()
+ }
+ fn __repr__(&self) -> String {
+ format!("Footprint(tick_size={})", self.inner.tick_size())
+ }
+}
+
// ============================== Family 15: Risk / Performance ==============================
#[pyclass(name = "SharpeRatio", module = "wickra._wickra", skip_from_py_object)]
@@ -13215,6 +13302,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ // Microstructure: footprint.
+ m.add_class::()?;
// Family 15: Risk / Performance metrics.
m.add_class::()?;
m.add_class::()?;
diff --git a/bindings/python/tests/test_input_validation.py b/bindings/python/tests/test_input_validation.py
index c669b91c..85bddce2 100644
--- a/bindings/python/tests/test_input_validation.py
+++ b/bindings/python/tests/test_input_validation.py
@@ -231,3 +231,10 @@ def test_realized_spread_zero_horizon_raises():
def test_kyles_lambda_window_below_two_raises():
with pytest.raises(ValueError):
ta.KylesLambda(1)
+
+
+def test_footprint_non_positive_tick_raises():
+ with pytest.raises(ValueError):
+ ta.Footprint(0.0)
+ with pytest.raises(ValueError):
+ ta.Footprint(-1.0)
diff --git a/bindings/python/tests/test_known_values.py b/bindings/python/tests/test_known_values.py
index 38b67d06..14aa7ff5 100644
--- a/bindings/python/tests/test_known_values.py
+++ b/bindings/python/tests/test_known_values.py
@@ -882,6 +882,17 @@ def test_depth_slope_reference_value():
assert ta.DepthSlope().update([100.0], [1.0], [101.0], [1.0]) == pytest.approx(0.0)
+def test_footprint_buckets_buy_and_sell_volume():
+ fp = ta.Footprint(1.0)
+ fp.update(100.2, 2.0, True) # bucket 100 -> ask 2
+ fp.update(100.7, 3.0, False) # bucket 101 -> bid 3
+ out = fp.update(100.1, 1.0, True) # bucket 100 -> ask 3
+ # Columns are [price, bid_vol, ask_vol], rows sorted ascending by price.
+ assert out.shape == (2, 3)
+ assert list(out[0]) == [100.0, 0.0, 3.0]
+ assert list(out[1]) == [101.0, 3.0, 0.0]
+
+
def test_signed_volume_reference_values():
assert ta.SignedVolume().update(100.0, 2.0, True) == pytest.approx(2.0)
assert ta.SignedVolume().update(100.0, 3.0, False) == pytest.approx(-3.0)
diff --git a/bindings/python/tests/test_lifecycle.py b/bindings/python/tests/test_lifecycle.py
index abc866c2..e9f125e5 100644
--- a/bindings/python/tests/test_lifecycle.py
+++ b/bindings/python/tests/test_lifecycle.py
@@ -207,3 +207,14 @@ def test_kyles_lambda_lifecycle_and_repr():
kl.reset()
assert not kl.is_ready()
assert repr(ta.KylesLambda(7)) == "KylesLambda(window=7)"
+
+
+def test_footprint_lifecycle_and_repr():
+ fp = ta.Footprint(0.5)
+ assert fp.warmup_period() == 1
+ assert not fp.is_ready()
+ fp.update(100.0, 1.0, True)
+ assert fp.is_ready()
+ fp.reset()
+ assert not fp.is_ready()
+ assert repr(ta.Footprint(0.25)) == "Footprint(tick_size=0.25)"
diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py
index 9bc4d2a8..c2a8754d 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -1939,3 +1939,16 @@ def test_price_impact_indicators_streaming_equals_batch():
)
assert batch.shape == (n,)
assert _eq_nan(batch, streamed)
+
+
+def test_footprint_streaming_equals_batch():
+ n = 20
+ price = [100.0 + (i % 5) * 0.3 for i in range(n)]
+ size = [1.0 + (i % 3) for i in range(n)]
+ is_buy = [i % 2 == 0 for i in range(n)]
+ batch = ta.Footprint(1.0).batch(price, size, is_buy)
+ streamer = ta.Footprint(1.0)
+ assert len(batch) == n
+ for i in range(n):
+ streamed = streamer.update(price[i], size[i], is_buy[i])
+ assert np.array_equal(streamed, batch[i])
diff --git a/bindings/python/tests/test_smoke.py b/bindings/python/tests/test_smoke.py
index c3528822..9845ba76 100644
--- a/bindings/python/tests/test_smoke.py
+++ b/bindings/python/tests/test_smoke.py
@@ -154,3 +154,16 @@ def test_price_impact_batch_returns_one_value_per_trade():
out = ind.batch(price, size, is_buy, mid)
assert out.shape == (4,)
assert out.dtype == np.float64
+
+
+def test_footprint_constructs_and_emits():
+ out = ta.Footprint(1.0).update(100.2, 2.0, True)
+ assert out.shape == (1, 3)
+ assert out.dtype == np.float64
+
+
+def test_footprint_batch_returns_list_of_arrays():
+ res = ta.Footprint(1.0).batch([100.2, 100.7], [2.0, 3.0], [True, False])
+ assert isinstance(res, list)
+ assert len(res) == 2
+ assert res[-1].shape[1] == 3
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 94865596..34ed1ad2 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -9,7 +9,7 @@
#![allow(clippy::needless_pass_by_value)]
#![allow(missing_debug_implementations)] // wasm_bindgen wrappers expose JS objects, no need for Debug
-use js_sys::{Float64Array, Object, Reflect};
+use js_sys::{Array, Float64Array, Object, Reflect};
use wasm_bindgen::prelude::*;
use wickra_core as wc;
use wickra_core::{BatchExt, Indicator};
@@ -6699,6 +6699,54 @@ impl WasmKylesLambda {
}
}
+// ============================== Microstructure: Footprint ==============================
+//
+// Footprint is a multi-output, variable-length indicator. Each `update(price,
+// size, isBuy)` returns the full bar footprint accumulated since the last
+// `reset()` as an array of `{ price, bidVol, askVol }` objects (sorted ascending
+// by price) — the streaming model for a live browser trade feed.
+
+#[wasm_bindgen(js_name = Footprint)]
+pub struct WasmFootprint {
+ inner: wc::Footprint,
+}
+
+#[wasm_bindgen(js_class = Footprint)]
+impl WasmFootprint {
+ #[wasm_bindgen(constructor)]
+ pub fn new(tick_size: f64) -> Result {
+ Ok(Self {
+ inner: wc::Footprint::new(tick_size).map_err(map_err)?,
+ })
+ }
+ pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result {
+ let out = self
+ .inner
+ .update(build_trade(price, size, is_buy)?)
+ .expect("footprint emits on every trade");
+ let levels = Array::new();
+ for level in &out.levels {
+ let obj = Object::new();
+ Reflect::set(&obj, &"price".into(), &level.price.into()).ok();
+ Reflect::set(&obj, &"bidVol".into(), &level.bid_vol.into()).ok();
+ Reflect::set(&obj, &"askVol".into(), &level.ask_vol.into()).ok();
+ levels.push(&obj);
+ }
+ Ok(levels.into())
+ }
+ pub fn reset(&mut self) {
+ self.inner.reset();
+ }
+ #[wasm_bindgen(js_name = isReady)]
+ pub fn is_ready(&self) -> bool {
+ self.inner.is_ready()
+ }
+ #[wasm_bindgen(js_name = warmupPeriod)]
+ pub fn warmup_period(&self) -> usize {
+ self.inner.warmup_period()
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/wickra-core/src/indicators/footprint.rs b/crates/wickra-core/src/indicators/footprint.rs
new file mode 100644
index 00000000..2e5d38e4
--- /dev/null
+++ b/crates/wickra-core/src/indicators/footprint.rs
@@ -0,0 +1,259 @@
+//! Footprint — buy/sell volume profile per price bucket within a bar.
+
+use std::collections::BTreeMap;
+
+use crate::error::{Error, Result};
+use crate::microstructure::Trade;
+use crate::traits::Indicator;
+
+/// One price bucket of a [`Footprint`]: the buy- and sell-initiated volume that
+/// traded there since the last reset.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct FootprintLevel {
+ /// Bucket price (the bucket index times the tick size).
+ pub price: f64,
+ /// Sell-initiated (bid-hitting) volume traded at this bucket.
+ pub bid_vol: f64,
+ /// Buy-initiated (ask-lifting) volume traded at this bucket.
+ pub ask_vol: f64,
+}
+
+/// The full footprint of a bar: one [`FootprintLevel`] per touched price
+/// bucket, sorted ascending by price.
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct FootprintOutput {
+ /// Touched price buckets, lowest price first.
+ pub levels: Vec,
+}
+
+/// Footprint — the buy/sell volume profile of a bar, bucketed by price.
+///
+/// A footprint (a.k.a. bid/ask or volume cluster chart) decomposes the volume
+/// traded within a bar across the price levels at which it printed, splitting
+/// each level into buy-initiated (ask-lifting) and sell-initiated (bid-hitting)
+/// volume. It exposes *where* inside a bar the activity happened and which side
+/// was the aggressor there — the basis for absorption, imbalance and
+/// point-of-control analysis that a single OHLCV bar hides.
+///
+/// Each trade is assigned to the price bucket `round(price / tick_size)`; its
+/// size is added to that bucket's ask volume for a buy and bid volume for a
+/// sell. Every [`update`] returns the complete footprint accumulated since the
+/// last [`reset`], as a [`FootprintOutput`] whose `levels` are sorted ascending
+/// by price. Call [`reset`] at each bar (or session) boundary to start a fresh
+/// footprint.
+///
+/// `Input = Trade`, `Output = FootprintOutput`. Ready after the first trade.
+///
+/// [`update`]: crate::Indicator::update
+/// [`reset`]: crate::Indicator::reset
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Footprint, Indicator, Side, Trade};
+///
+/// let mut fp = Footprint::new(1.0).unwrap();
+/// fp.update(Trade::new(100.2, 2.0, Side::Buy, 0).unwrap());
+/// let out = fp.update(Trade::new(100.7, 3.0, Side::Sell, 1).unwrap()).unwrap();
+/// // Two buckets: 100 (ask 2) and 101 (bid 3).
+/// assert_eq!(out.levels.len(), 2);
+/// assert_eq!(out.levels[0].price, 100.0);
+/// assert_eq!(out.levels[0].ask_vol, 2.0);
+/// assert_eq!(out.levels[1].price, 101.0);
+/// assert_eq!(out.levels[1].bid_vol, 3.0);
+/// ```
+#[derive(Debug, Clone)]
+pub struct Footprint {
+ tick_size: f64,
+ // bucket index -> (bid_vol = sell-initiated, ask_vol = buy-initiated).
+ buckets: BTreeMap,
+ has_emitted: bool,
+}
+
+impl Footprint {
+ /// Construct a footprint with the given price-bucket `tick_size`.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidTick`] if `tick_size` is not a finite, strictly
+ /// positive number.
+ pub fn new(tick_size: f64) -> Result {
+ if !tick_size.is_finite() || tick_size <= 0.0 {
+ return Err(Error::InvalidTick {
+ message: "footprint tick_size must be finite and positive",
+ });
+ }
+ Ok(Self {
+ tick_size,
+ buckets: BTreeMap::new(),
+ has_emitted: false,
+ })
+ }
+
+ /// The configured price-bucket size.
+ pub const fn tick_size(&self) -> f64 {
+ self.tick_size
+ }
+
+ fn bucket_index(&self, price: f64) -> i64 {
+ // Float-to-int `as` saturates rather than wrapping, so an extreme
+ // price/tick ratio clamps to i64::MIN/MAX instead of misbehaving;
+ // realistic ratios fit comfortably.
+ #[allow(clippy::cast_possible_truncation)]
+ {
+ (price / self.tick_size).round() as i64
+ }
+ }
+
+ fn snapshot(&self) -> FootprintOutput {
+ let levels = self
+ .buckets
+ .iter()
+ .map(|(&index, &(bid_vol, ask_vol))| FootprintLevel {
+ price: index as f64 * self.tick_size,
+ bid_vol,
+ ask_vol,
+ })
+ .collect();
+ FootprintOutput { levels }
+ }
+}
+
+impl Indicator for Footprint {
+ type Input = Trade;
+ type Output = FootprintOutput;
+
+ fn update(&mut self, trade: Trade) -> Option {
+ self.has_emitted = true;
+ let index = self.bucket_index(trade.price);
+ let entry = self.buckets.entry(index).or_insert((0.0, 0.0));
+ if trade.side.sign() > 0.0 {
+ entry.1 += trade.size;
+ } else {
+ entry.0 += trade.size;
+ }
+ Some(self.snapshot())
+ }
+
+ fn reset(&mut self) {
+ self.buckets.clear();
+ self.has_emitted = false;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 1
+ }
+
+ fn is_ready(&self) -> bool {
+ self.has_emitted
+ }
+
+ fn name(&self) -> &'static str {
+ "Footprint"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::microstructure::Side;
+ use crate::traits::BatchExt;
+
+ fn trade(price: f64, size: f64, side: Side) -> Trade {
+ Trade::new(price, size, side, 0).unwrap()
+ }
+
+ #[test]
+ fn rejects_bad_tick_size() {
+ assert!(matches!(
+ Footprint::new(0.0),
+ Err(Error::InvalidTick { .. })
+ ));
+ assert!(matches!(
+ Footprint::new(-1.0),
+ Err(Error::InvalidTick { .. })
+ ));
+ assert!(matches!(
+ Footprint::new(f64::NAN),
+ Err(Error::InvalidTick { .. })
+ ));
+ assert!(Footprint::new(0.5).is_ok());
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let fp = Footprint::new(0.25).unwrap();
+ assert_eq!(fp.name(), "Footprint");
+ assert_eq!(fp.warmup_period(), 1);
+ assert_eq!(fp.tick_size(), 0.25);
+ assert!(!fp.is_ready());
+ }
+
+ #[test]
+ fn buckets_buy_and_sell_volume() {
+ let mut fp = Footprint::new(1.0).unwrap();
+ fp.update(trade(100.2, 2.0, Side::Buy));
+ fp.update(trade(100.7, 3.0, Side::Sell));
+ let out = fp.update(trade(100.1, 1.0, Side::Buy)).unwrap();
+ assert!(fp.is_ready());
+ // Bucket 100: buy 2 + buy 1 = ask 3, bid 0. Bucket 101: sell 3.
+ assert_eq!(out.levels.len(), 2);
+ assert_eq!(out.levels[0].price, 100.0);
+ assert_eq!(out.levels[0].ask_vol, 3.0);
+ assert_eq!(out.levels[0].bid_vol, 0.0);
+ assert_eq!(out.levels[1].price, 101.0);
+ assert_eq!(out.levels[1].bid_vol, 3.0);
+ assert_eq!(out.levels[1].ask_vol, 0.0);
+ }
+
+ #[test]
+ fn levels_sorted_ascending_by_price() {
+ let mut fp = Footprint::new(1.0).unwrap();
+ fp.update(trade(103.0, 1.0, Side::Buy));
+ fp.update(trade(100.0, 1.0, Side::Sell));
+ let out = fp.update(trade(101.0, 1.0, Side::Buy)).unwrap();
+ let prices: Vec = out.levels.iter().map(|l| l.price).collect();
+ assert_eq!(prices, vec![100.0, 101.0, 103.0]);
+ }
+
+ #[test]
+ fn sub_tick_prices_share_a_bucket() {
+ let mut fp = Footprint::new(0.5).unwrap();
+ // 100.24 and 100.26 both round to bucket 200 (price 100.0)... check:
+ // 100.24/0.5 = 200.48 -> 200; 100.26/0.5 = 200.52 -> 201. Distinct.
+ fp.update(trade(100.20, 1.0, Side::Buy)); // 200.4 -> 200 -> price 100.0
+ let out = fp.update(trade(100.10, 2.0, Side::Buy)).unwrap(); // 200.2 -> 200
+ assert_eq!(out.levels.len(), 1);
+ assert_eq!(out.levels[0].price, 100.0);
+ assert_eq!(out.levels[0].ask_vol, 3.0);
+ }
+
+ #[test]
+ fn reset_clears_the_footprint() {
+ let mut fp = Footprint::new(1.0).unwrap();
+ fp.update(trade(100.0, 5.0, Side::Buy));
+ assert!(fp.is_ready());
+ fp.reset();
+ assert!(!fp.is_ready());
+ let out = fp.update(trade(200.0, 1.0, Side::Sell)).unwrap();
+ assert_eq!(out.levels.len(), 1);
+ assert_eq!(out.levels[0].price, 200.0);
+ assert_eq!(out.levels[0].bid_vol, 1.0);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let trades: Vec = (0..30)
+ .map(|i| {
+ let side = if i % 3 == 0 { Side::Sell } else { Side::Buy };
+ trade(100.0 + f64::from(i % 5), 1.0 + f64::from(i % 4), side)
+ })
+ .collect();
+ let mut a = Footprint::new(1.0).unwrap();
+ let mut b = Footprint::new(1.0).unwrap();
+ assert_eq!(
+ a.batch(&trades),
+ trades.iter().map(|x| b.update(*x)).collect::>()
+ );
+ }
+}
diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index f87df01c..18c33eab 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -73,6 +73,7 @@ mod evwma;
mod fama;
mod fibonacci_pivots;
mod fisher_transform;
+mod footprint;
mod force_index;
mod fractal_chaos_bands;
mod frama;
@@ -304,6 +305,7 @@ pub use evwma::Evwma;
pub use fama::Fama;
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
pub use fisher_transform::FisherTransform;
+pub use footprint::{Footprint, FootprintLevel, FootprintOutput};
pub use force_index::ForceIndex;
pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
pub use frama::Frama;
@@ -746,6 +748,7 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"EffectiveSpread",
"RealizedSpread",
"KylesLambda",
+ "Footprint",
],
),
(
@@ -802,6 +805,6 @@ mod family_tests {
// the actual indicator count is the early-warning signal that an
// indicator was added without being assigned a family.
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
- assert_eq!(total, 226, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 227, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs
index d287df7b..2f33c4ce 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -59,19 +59,20 @@ pub use indicators::{
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo,
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
- FisherTransform, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, GainLossRatio,
- GarmanKlassVolatility, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
- HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput,
- HurstExponent, Ichimoku, IchimokuOutput, Inertia, InformationRatio, InitialBalance,
- InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma,
- Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
- LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
- LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, MaEnvelope,
- MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
- Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
- Microprice, Mom, MorningEveningStar, Natr, Nvi, Obv, OmegaRatio, OpeningRange,
- OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
- PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
+ FisherTransform, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
+ FractalChaosBandsOutput, Frama, GainLossRatio, GarmanKlassVolatility, Hammer, HangingMan,
+ Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator, HilbertDominantCycle,
+ HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku,
+ IchimokuOutput, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
+ InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion,
+ Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LaguerreRsi,
+ LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
+ LinRegChannelOutput, LinRegSlope, LinearRegression, MaEnvelope, MaEnvelopeOutput,
+ MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
+ MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom,
+ MorningEveningStar, Natr, Nvi, Obv, OmegaRatio, OpeningRange, OpeningRangeOutput,
+ OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex,
+ PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
PercentageTrailingStop, Pgo, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
RelativeStrengthOutput, RenkoTrailingStop, Roc, RogersSatchellVolatility, RollingVwap,
@@ -90,6 +91,10 @@ pub use indicators::{
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
};
+// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
+// line so the indicator-count tooling (which scans the braced block above and
+// strips only `*Output` companions) does not count it as a separate indicator.
+pub use indicators::FootprintLevel;
pub use microstructure::{Level, OrderBook, Side, Trade, TradeQuote};
pub use ohlcv::{Candle, Tick};
pub use traits::{BatchExt, Chain, Indicator};
diff --git a/fuzz/fuzz_targets/indicator_update_trade.rs b/fuzz/fuzz_targets/indicator_update_trade.rs
index a436a745..898529e9 100644
--- a/fuzz/fuzz_targets/indicator_update_trade.rs
+++ b/fuzz/fuzz_targets/indicator_update_trade.rs
@@ -11,7 +11,8 @@
use libfuzzer_sys::fuzz_target;
use wickra_core::{
- BatchExt, CumulativeVolumeDelta, Indicator, Side, SignedVolume, Trade, TradeImbalance,
+ BatchExt, CumulativeVolumeDelta, Footprint, Indicator, Side, SignedVolume, Trade,
+ TradeImbalance,
};
#[inline(never)]
@@ -42,4 +43,13 @@ fuzz_target!(|data: &[u8]| {
drive(SignedVolume::new, &trades);
drive(CumulativeVolumeDelta::new, &trades);
drive(|| TradeImbalance::new(5).unwrap(), &trades);
+
+ // Footprint emits a variable-length `FootprintOutput` rather than an `f64`,
+ // so it is driven directly rather than through the scalar-output helper.
+ let mut footprint = Footprint::new(0.5).unwrap();
+ for &trade in &trades {
+ let _ = footprint.update(trade);
+ }
+ footprint.reset();
+ let _ = Footprint::new(0.5).unwrap().batch(&trades);
});