feat: footprint microstructure indicator (part 4 of 4) (#123)
This commit is contained in:
@@ -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));
|
||||
});
|
||||
|
||||
Vendored
+15
@@ -286,6 +286,12 @@ export interface ObSnapshot {
|
||||
askPx: Array<number>
|
||||
askSz: Array<number>
|
||||
}
|
||||
/** 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<FootprintLevelValue>
|
||||
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<Array<FootprintLevelValue>>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SharpeRatioNode = SharpeRatio
|
||||
export declare class SharpeRatio {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FootprintLevelValue> {
|
||||
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<Self> {
|
||||
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<Vec<FootprintLevelValue>> {
|
||||
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<f64>,
|
||||
size: Vec<f64>,
|
||||
is_buy: Vec<bool>,
|
||||
) -> napi::Result<Vec<Vec<FootprintLevelValue>>> {
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<f64>> {
|
||||
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<Self> {
|
||||
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<Bound<'py, PyArray2<f64>>> {
|
||||
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<f64>,
|
||||
size: Vec<f64>,
|
||||
is_buy: Vec<bool>,
|
||||
) -> PyResult<Vec<Bound<'py, PyArray2<f64>>>> {
|
||||
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::<PyEffectiveSpread>()?;
|
||||
m.add_class::<PyRealizedSpread>()?;
|
||||
m.add_class::<PyKylesLambda>()?;
|
||||
// Microstructure: footprint.
|
||||
m.add_class::<PyFootprint>()?;
|
||||
// Family 15: Risk / Performance metrics.
|
||||
m.add_class::<PySharpeRatio>()?;
|
||||
m.add_class::<PySortinoRatio>()?;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WasmFootprint, JsError> {
|
||||
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<JsValue, JsError> {
|
||||
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::*;
|
||||
|
||||
Reference in New Issue
Block a user