feat: order-book microstructure indicators (part 1 of 4) (#112)
* feat(core): add microstructure input types (OrderBook, Trade, TradeQuote) New non-OHLCV value types for the order-book / trade-flow indicator family: Level, OrderBook (sorted, uncrossed depth snapshot), Side, Trade (with aggressor side), and TradeQuote (trade paired with prevailing mid). Each has a validating constructor plus a new_unchecked hot-path constructor, with full unit coverage. Adds InvalidOrderBook / InvalidTrade error variants. * feat(core): add 5 order-book microstructure indicators OrderBookImbalanceTop1/TopN/Full (signed depth imbalance), Microprice (size-weighted fair value), and QuotedSpread (top-of-book spread in bps). All consume the OrderBook snapshot type, emit f64, are stateless and ready after the first snapshot, with full unit coverage. Registers a new Microstructure family in the taxonomy. * feat(bindings): expose order-book microstructure indicators Python, Node, and WASM bindings for OrderBookImbalanceTop1/TopN/Full, Microprice and QuotedSpread. Each takes a depth snapshot via four equal-length (bid_px, bid_sz, ask_px, ask_sz) arrays. Python and Node expose a batch over a list of snapshots; WASM exposes per-snapshot update (the streaming model that fits a browser book feed). Regenerates node index.d.ts/.js and registers the new InvalidOrderBook/InvalidTrade arms in the Python error mapping. * test(bindings,fuzz): cover order-book microstructure indicators Python: smoke, reference values, streaming-vs-batch, lifecycle/repr and input validation (mismatched lengths, crossed book, misordered levels, zero levels) for all five order-book indicators. Node: reference values, streaming-vs-batch, and rejection cases. Adds an indicator_update_orderbook fuzz target driving every order-book indicator over arbitrary (incl. degenerate) snapshots. * bench(microstructure): synthetic order-book benchmarks Add a bench_orderbook_input harness and synthesise a five-level book around each candle close (no order-book dataset ships with the repo). Benches the cheapest (top-of-book imbalance) and most-expensive (full-depth imbalance) plus microprice, matching the curated cheapest/expensive-per-family approach. * docs: add Microstructure family + bump indicator counter to 224 README gains the Microstructure family row (order-book imbalance, microprice, quoted spread) and the indicator counter goes 219 -> 224 across seventeen families; CHANGELOG records the new order-book indicators and value types.
This commit is contained in:
@@ -912,3 +912,42 @@ test('Doji signed mode encodes dragonfly/gravestone/neutral direction', () => {
|
||||
assert.equal(d.update(10, 12, 8, 10), 0); // long-legged -> neutral 0
|
||||
assert.equal(d.update(10, 12, 10, 12), 0); // not a doji -> 0
|
||||
});
|
||||
|
||||
test('order-book indicators reference values', () => {
|
||||
// Top-1: (3 - 1) / (3 + 1) = 0.5.
|
||||
assert.equal(new wickra.OrderBookImbalanceTop1().update([100], [3], [101], [1]), 0.5);
|
||||
// Top-2: bidDepth 3, askDepth 2 -> (3 - 2) / 5 = 0.2.
|
||||
assert.ok(
|
||||
Math.abs(new wickra.OrderBookImbalanceTopN(2).update([100, 99], [2, 1], [101, 102], [1, 1]) - 0.2) < 1e-12,
|
||||
);
|
||||
// Full: bidDepth 1, askDepth 3 -> -0.5.
|
||||
assert.equal(new wickra.OrderBookImbalanceFull().update([100], [1], [101, 102], [2, 1]), -0.5);
|
||||
// Microprice: (100*3 + 101*1) / 4 = 100.25.
|
||||
assert.equal(new wickra.Microprice().update([100], [1], [101], [3]), 100.25);
|
||||
// Quoted spread: 1 / 100.5 * 10000 ≈ 99.5025 bps.
|
||||
assert.ok(Math.abs(new wickra.QuotedSpread().update([100], [1], [101], [1]) - 99.50248756) < 1e-6);
|
||||
});
|
||||
|
||||
test('order-book streaming update matches batch', () => {
|
||||
const snaps = Array.from({ length: 30 }, (_, i) => ({
|
||||
bidPx: [100, 99],
|
||||
bidSz: [1 + (i % 5), 1],
|
||||
askPx: [101, 102],
|
||||
askSz: [1 + ((i + 1) % 3), 1],
|
||||
}));
|
||||
const batch = new wickra.Microprice().batch(snaps);
|
||||
const streamer = new wickra.Microprice();
|
||||
assert.equal(batch.length, snaps.length);
|
||||
for (let i = 0; i < snaps.length; i++) {
|
||||
const s = streamer.update(snaps[i].bidPx, snaps[i].bidSz, snaps[i].askPx, snaps[i].askSz);
|
||||
assert.ok(Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}: ${s} vs ${batch[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('order-book TopN rejects zero levels', () => {
|
||||
assert.throws(() => new wickra.OrderBookImbalanceTopN(0));
|
||||
});
|
||||
|
||||
test('order-book update rejects a crossed book', () => {
|
||||
assert.throws(() => new wickra.QuotedSpread().update([102], [1], [101], [1]));
|
||||
});
|
||||
|
||||
Vendored
+52
@@ -279,6 +279,13 @@ export interface OpeningRangeValue {
|
||||
low: number
|
||||
breakoutDistance: number
|
||||
}
|
||||
/** One order-book depth snapshot for batch evaluation. */
|
||||
export interface ObSnapshot {
|
||||
bidPx: Array<number>
|
||||
bidSz: Array<number>
|
||||
askPx: Array<number>
|
||||
askSz: Array<number>
|
||||
}
|
||||
export type SmaNode = SMA
|
||||
export declare class SMA {
|
||||
constructor(period: number)
|
||||
@@ -2189,6 +2196,51 @@ export declare class ThreeOutside {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
|
||||
export declare class OrderBookImbalanceTop1 {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceFullNode = OrderBookImbalanceFull
|
||||
export declare class OrderBookImbalanceFull {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MicropriceNode = Microprice
|
||||
export declare class Microprice {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type QuotedSpreadNode = QuotedSpread
|
||||
export declare class QuotedSpread {
|
||||
constructor()
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OrderBookImbalanceTopNNode = OrderBookImbalanceTopN
|
||||
export declare class OrderBookImbalanceTopN {
|
||||
constructor(levels: number)
|
||||
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||
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, 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, OrderBookImbalanceTopN, 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
|
||||
@@ -515,6 +515,11 @@ module.exports.Tweezer = Tweezer
|
||||
module.exports.SpinningTop = SpinningTop
|
||||
module.exports.ThreeInside = ThreeInside
|
||||
module.exports.ThreeOutside = ThreeOutside
|
||||
module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
||||
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||
module.exports.Microprice = Microprice
|
||||
module.exports.QuotedSpread = QuotedSpread
|
||||
module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
|
||||
module.exports.SharpeRatio = SharpeRatio
|
||||
module.exports.SortinoRatio = SortinoRatio
|
||||
module.exports.CalmarRatio = CalmarRatio
|
||||
|
||||
@@ -8754,6 +8754,166 @@ node_candle_pattern!(SpinningTopNode, wc::SpinningTop, "SpinningTop");
|
||||
node_candle_pattern!(ThreeInsideNode, wc::ThreeInside, "ThreeInside");
|
||||
node_candle_pattern!(ThreeOutsideNode, wc::ThreeOutside, "ThreeOutside");
|
||||
|
||||
// ============================== Microstructure: Order Book ==============================
|
||||
//
|
||||
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
|
||||
// `update(bidPx, bidSz, askPx, askSz)` takes four equal-length arrays for one
|
||||
// snapshot (bids best-first = descending price, asks best-first = ascending
|
||||
// price); `batch` takes an array of `{ bidPx, bidSz, askPx, askSz }` snapshots
|
||||
// and returns one value per snapshot.
|
||||
|
||||
/// One order-book depth snapshot for batch evaluation.
|
||||
#[napi(object)]
|
||||
pub struct ObSnapshot {
|
||||
pub bid_px: Vec<f64>,
|
||||
pub bid_sz: Vec<f64>,
|
||||
pub ask_px: Vec<f64>,
|
||||
pub ask_sz: Vec<f64>,
|
||||
}
|
||||
|
||||
fn build_order_book(
|
||||
bid_px: &[f64],
|
||||
bid_sz: &[f64],
|
||||
ask_px: &[f64],
|
||||
ask_sz: &[f64],
|
||||
) -> napi::Result<wc::OrderBook> {
|
||||
if bid_px.len() != bid_sz.len() || ask_px.len() != ask_sz.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"bid/ask price and size arrays must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let bids = bid_px
|
||||
.iter()
|
||||
.zip(bid_sz)
|
||||
.map(|(&p, &s)| wc::Level::new_unchecked(p, s))
|
||||
.collect();
|
||||
let asks = ask_px
|
||||
.iter()
|
||||
.zip(ask_sz)
|
||||
.map(|(&p, &s)| wc::Level::new_unchecked(p, s))
|
||||
.collect();
|
||||
wc::OrderBook::new(bids, asks).map_err(map_err)
|
||||
}
|
||||
|
||||
macro_rules! node_ob_indicator {
|
||||
($node:ident, $inner:ty, $js:literal) => {
|
||||
#[napi(js_name = $js)]
|
||||
pub struct $node {
|
||||
inner: $inner,
|
||||
}
|
||||
|
||||
impl Default for $node {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl $node {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: <$inner>::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
bid_px: Vec<f64>,
|
||||
bid_sz: Vec<f64>,
|
||||
ask_px: Vec<f64>,
|
||||
ask_sz: Vec<f64>,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
let book = build_order_book(&bid_px, &bid_sz, &ask_px, &ask_sz)?;
|
||||
Ok(self.inner.update(book))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, snapshots: Vec<ObSnapshot>) -> napi::Result<Vec<f64>> {
|
||||
let mut out = Vec::with_capacity(snapshots.len());
|
||||
for snap in &snapshots {
|
||||
let book =
|
||||
build_order_book(&snap.bid_px, &snap.bid_sz, &snap.ask_px, &snap.ask_sz)?;
|
||||
out.push(self.inner.update(book).unwrap_or(f64::NAN));
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
node_ob_indicator!(
|
||||
OrderBookImbalanceTop1Node,
|
||||
wc::OrderBookImbalanceTop1,
|
||||
"OrderBookImbalanceTop1"
|
||||
);
|
||||
node_ob_indicator!(
|
||||
OrderBookImbalanceFullNode,
|
||||
wc::OrderBookImbalanceFull,
|
||||
"OrderBookImbalanceFull"
|
||||
);
|
||||
node_ob_indicator!(MicropriceNode, wc::Microprice, "Microprice");
|
||||
node_ob_indicator!(QuotedSpreadNode, wc::QuotedSpread, "QuotedSpread");
|
||||
|
||||
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
||||
#[napi(js_name = "OrderBookImbalanceTopN")]
|
||||
pub struct OrderBookImbalanceTopNNode {
|
||||
inner: wc::OrderBookImbalanceTopN,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OrderBookImbalanceTopNNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(levels: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::OrderBookImbalanceTopN::new(levels as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
bid_px: Vec<f64>,
|
||||
bid_sz: Vec<f64>,
|
||||
ask_px: Vec<f64>,
|
||||
ask_sz: Vec<f64>,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
let book = build_order_book(&bid_px, &bid_sz, &ask_px, &ask_sz)?;
|
||||
Ok(self.inner.update(book))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, snapshots: Vec<ObSnapshot>) -> napi::Result<Vec<f64>> {
|
||||
let mut out = Vec::with_capacity(snapshots.len());
|
||||
for snap in &snapshots {
|
||||
let book = build_order_book(&snap.bid_px, &snap.bid_sz, &snap.ask_px, &snap.ask_sz)?;
|
||||
out.push(self.inner.update(book).unwrap_or(f64::NAN));
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user