Add 10 pairwise stat-arb indicators to Price Statistics (#154)
Adds ten pairwise `(f64, f64)` indicators to the **Price Statistics** family, completing the A1 stat-arb expansion block.
## Indicators
**Scalar output:**
- **RollingCorrelation** — rolling Pearson correlation of period-over-period *returns* (distinct from level-based `PearsonCorrelation`).
- **RollingCovariance** — rolling covariance of returns.
- **OuHalfLife** — Ornstein–Uhlenbeck half-life of mean reversion of the spread `a − b`.
- **SpreadHurst** — Hurst exponent of the spread (variance-of-lagged-differences fit) for regime detection.
- **DistanceSsd** — Gatev sum-of-squared-deviations between two start-normalised series.
- **BetaNeutralSpread** — rolling OLS regression residual `a − (α + β·b)`.
- **VarianceRatio** — Lo–MacKinlay variance-ratio test on the spread (two params: `period`, `q`).
- **GrangerCausality** — F-statistic for whether `b` predicts `a` (two params: `period`, `lag`).
**Struct output (custom bindings):**
- **KalmanHedgeRatio** — dynamic hedge ratio via a Kalman filter → `{ hedgeRatio, intercept, spread }`.
- **SpreadBollingerBands** — Bollinger bands on the spread → `{ middle, upper, lower, percentB }`.
## Notes
- No new traits or input families: all use the native `Indicator<Input = (f64, f64)>` (precedent `Beta`, `Cointegration`).
- Adds `Error::InvalidParameter` for floating-point constructor parameters (Kalman `delta`/`observation_var`, `num_std`).
- Full Python/Node/WASM bindings; the two struct-output indicators are hand-written, the rest use the pair macros.
- Indicator count 315 → 325; README, family rows, `__init__`, fuzz target, and CHANGELOG updated.
## Verification
- `cargo test --workspace --all-features` — green (2676 core lib + 308 doc).
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean.
- Node: `npm run build && npm test` — 410 passing (`index.d.ts`/`index.js` regenerated).
- Python: `pytest` — 684 passing.
This commit is contained in:
@@ -529,6 +529,14 @@ const pairFactories = {
|
||||
PairwiseBeta: () => new wickra.PairwiseBeta(14),
|
||||
PairSpreadZScore: () => new wickra.PairSpreadZScore(14, 14),
|
||||
SpearmanCorrelation: () => new wickra.SpearmanCorrelation(14),
|
||||
RollingCorrelation: () => new wickra.RollingCorrelation(20),
|
||||
RollingCovariance: () => new wickra.RollingCovariance(20),
|
||||
OuHalfLife: () => new wickra.OuHalfLife(60),
|
||||
SpreadHurst: () => new wickra.SpreadHurst(60),
|
||||
DistanceSsd: () => new wickra.DistanceSsd(20),
|
||||
BetaNeutralSpread: () => new wickra.BetaNeutralSpread(20),
|
||||
VarianceRatio: () => new wickra.VarianceRatio(60, 2),
|
||||
GrangerCausality: () => new wickra.GrangerCausality(60, 1),
|
||||
};
|
||||
|
||||
for (const [name, make] of Object.entries(pairFactories)) {
|
||||
@@ -619,6 +627,47 @@ test('Cointegration batch is flat 3*n with last row matching', () => {
|
||||
assert.ok(out[3 * (n - 1) + 2] < -2);
|
||||
});
|
||||
|
||||
test('KalmanHedgeRatio converges to a static hedge ratio (object output)', () => {
|
||||
const n = 500;
|
||||
const b = Array.from({ length: n }, (_, t) => 100 + 95 * Math.sin(t * 0.5));
|
||||
const a = b.map((v) => 2 * v + 5);
|
||||
const k = new wickra.KalmanHedgeRatio(1e-2, 1e-3);
|
||||
let last = null;
|
||||
for (let i = 0; i < n; i++) last = k.update(a[i], b[i]);
|
||||
assert.ok(Math.abs(last.hedgeRatio - 2) < 0.05);
|
||||
assert.ok(Math.abs(last.spread) < 0.05);
|
||||
});
|
||||
|
||||
test('KalmanHedgeRatio batch is flat 3*n with last row matching', () => {
|
||||
const n = 500;
|
||||
const b = Array.from({ length: n }, (_, t) => 100 + 95 * Math.sin(t * 0.5));
|
||||
const a = b.map((v) => 2 * v + 5);
|
||||
const out = new wickra.KalmanHedgeRatio(1e-2, 1e-3).batch(a, b);
|
||||
assert.equal(out.length, 3 * n);
|
||||
assert.ok(Math.abs(out[3 * (n - 1)] - 2) < 0.05);
|
||||
assert.ok(Math.abs(out[3 * (n - 1) + 2]) < 0.05);
|
||||
});
|
||||
|
||||
test('SpreadBollingerBands bands are ordered (object output)', () => {
|
||||
const n = 60;
|
||||
const b = Array.from({ length: n }, (_, t) => 100 + t);
|
||||
const a = b.map((v, t) => v + 3 * Math.sin(t * 0.4));
|
||||
const bb = new wickra.SpreadBollingerBands(20, 2.0);
|
||||
let last = null;
|
||||
for (let i = 0; i < n; i++) last = bb.update(a[i], b[i]);
|
||||
assert.ok(last.lower <= last.middle && last.middle <= last.upper);
|
||||
});
|
||||
|
||||
test('SpreadBollingerBands batch is flat 4*n with last row matching', () => {
|
||||
const n = 60;
|
||||
const b = Array.from({ length: n }, (_, t) => 100 + t);
|
||||
const a = b.map((v, t) => v + 3 * Math.sin(t * 0.4));
|
||||
const out = new wickra.SpreadBollingerBands(20, 2.0).batch(a, b);
|
||||
assert.equal(out.length, 4 * n);
|
||||
const base = 4 * (n - 1);
|
||||
assert.ok(out[base + 2] <= out[base] && out[base] <= out[base + 1]);
|
||||
});
|
||||
|
||||
test('RelativeStrengthAB constant ratio is flat (object output)', () => {
|
||||
const rs = new wickra.RelativeStrengthAB(5, 5);
|
||||
let last = null;
|
||||
|
||||
Vendored
+160
@@ -33,6 +33,26 @@ export interface RelativeStrengthValue {
|
||||
/** RSI of the ratio. */
|
||||
ratioRsi: number
|
||||
}
|
||||
/** Kalman hedge-ratio result: dynamic hedge ratio, intercept, and spread. */
|
||||
export interface KalmanHedgeRatioValue {
|
||||
/** Current hedge ratio (filtered slope of `a` on `b`). */
|
||||
hedgeRatio: number
|
||||
/** Current intercept (filtered level offset). */
|
||||
intercept: number
|
||||
/** Forecast error `a - (intercept + hedgeRatio*b)` — the spread signal. */
|
||||
spread: number
|
||||
}
|
||||
/** Spread Bollinger-bands result: middle, upper and lower bands plus `%b`. */
|
||||
export interface SpreadBollingerBandsValue {
|
||||
/** Middle band: the rolling mean of the spread. */
|
||||
middle: number
|
||||
/** Upper band. */
|
||||
upper: number
|
||||
/** Lower band. */
|
||||
lower: number
|
||||
/** `%b`: where the spread sits across the band (`0` lower, `1` upper). */
|
||||
percentB: number
|
||||
}
|
||||
/** MACD triple: macd line, signal line, histogram. */
|
||||
export interface MacdValue {
|
||||
macd: number
|
||||
@@ -786,6 +806,84 @@ export declare class SpearmanCorrelation {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RollingCorrelationNode = RollingCorrelation
|
||||
export declare class RollingCorrelation {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type RollingCovarianceNode = RollingCovariance
|
||||
export declare class RollingCovariance {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type OuHalfLifeNode = OuHalfLife
|
||||
export declare class OuHalfLife {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SpreadHurstNode = SpreadHurst
|
||||
export declare class SpreadHurst {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type DistanceSsdNode = DistanceSsd
|
||||
export declare class DistanceSsd {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BetaNeutralSpreadNode = BetaNeutralSpread
|
||||
export declare class BetaNeutralSpread {
|
||||
constructor(period: number)
|
||||
update(x: number, y: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array
|
||||
* with `NaN` for warmup positions.
|
||||
*/
|
||||
batch(x: Array<number>, y: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type PairSpreadZScoreNode = PairSpreadZScore
|
||||
/**
|
||||
* Pair spread z-score: two ctor params (`betaPeriod`, `zPeriod`), one `(a, b)`
|
||||
@@ -845,6 +943,68 @@ export declare class RelativeStrengthAB {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type VarianceRatioNode = VarianceRatio
|
||||
/**
|
||||
* Lo–MacKinlay variance ratio: two ctor params (`period`, `q`), one `(a, b)`
|
||||
* pair per update, a single ratio out.
|
||||
*/
|
||||
export declare class VarianceRatio {
|
||||
constructor(period: number, q: number)
|
||||
update(a: number, b: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array with
|
||||
* `NaN` for warmup positions.
|
||||
*/
|
||||
batch(a: Array<number>, b: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type GrangerCausalityNode = GrangerCausality
|
||||
/**
|
||||
* Granger causality F-statistic: two ctor params (`period`, `lag`), one
|
||||
* `(a, b)` pair per update, a single F-statistic out.
|
||||
*/
|
||||
export declare class GrangerCausality {
|
||||
constructor(period: number, lag: number)
|
||||
update(a: number, b: number): number | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a length-`n` array with
|
||||
* `NaN` for warmup positions.
|
||||
*/
|
||||
batch(a: Array<number>, b: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KalmanHedgeRatioNode = KalmanHedgeRatio
|
||||
export declare class KalmanHedgeRatio {
|
||||
constructor(delta: number, observationVar: number)
|
||||
update(a: number, b: number): KalmanHedgeRatioValue | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a flat array of length
|
||||
* `3 * n`, interleaved per row as `[hedgeRatio0, intercept0, spread0, ...]`.
|
||||
* Read column `j` of row `i` as `result[i * 3 + j]`. Warmup rows are `NaN`.
|
||||
*/
|
||||
batch(a: Array<number>, b: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type SpreadBollingerBandsNode = SpreadBollingerBands
|
||||
export declare class SpreadBollingerBands {
|
||||
constructor(period: number, numStd: number)
|
||||
update(a: number, b: number): SpreadBollingerBandsValue | null
|
||||
/**
|
||||
* Batch over two equally-sized arrays. Returns a flat array of length
|
||||
* `4 * n`, interleaved per row as `[middle0, upper0, lower0, percentB0, ...]`.
|
||||
* Read column `j` of row `i` as `result[i * 4 + j]`. Warmup rows are `NaN`.
|
||||
*/
|
||||
batch(a: Array<number>, b: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type MacdNode = MACD
|
||||
export declare class MACD {
|
||||
constructor(fast: number, slow: number, signal: number)
|
||||
|
||||
+11
-1
@@ -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, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, 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, AnchoredRSI, 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, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, 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, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, 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, AnchoredRSI, 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, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -362,10 +362,20 @@ module.exports.PearsonCorrelation = PearsonCorrelation
|
||||
module.exports.Beta = Beta
|
||||
module.exports.PairwiseBeta = PairwiseBeta
|
||||
module.exports.SpearmanCorrelation = SpearmanCorrelation
|
||||
module.exports.RollingCorrelation = RollingCorrelation
|
||||
module.exports.RollingCovariance = RollingCovariance
|
||||
module.exports.OuHalfLife = OuHalfLife
|
||||
module.exports.SpreadHurst = SpreadHurst
|
||||
module.exports.DistanceSsd = DistanceSsd
|
||||
module.exports.BetaNeutralSpread = BetaNeutralSpread
|
||||
module.exports.PairSpreadZScore = PairSpreadZScore
|
||||
module.exports.LeadLagCrossCorrelation = LeadLagCrossCorrelation
|
||||
module.exports.Cointegration = Cointegration
|
||||
module.exports.RelativeStrengthAB = RelativeStrengthAB
|
||||
module.exports.VarianceRatio = VarianceRatio
|
||||
module.exports.GrangerCausality = GrangerCausality
|
||||
module.exports.KalmanHedgeRatio = KalmanHedgeRatio
|
||||
module.exports.SpreadBollingerBands = SpreadBollingerBands
|
||||
module.exports.MACD = MACD
|
||||
module.exports.MACDFIX = MACDFIX
|
||||
module.exports.MACDEXT = MACDEXT
|
||||
|
||||
@@ -322,6 +322,24 @@ node_pair_indicator!(
|
||||
"SpearmanCorrelation",
|
||||
wc::SpearmanCorrelation
|
||||
);
|
||||
node_pair_indicator!(
|
||||
RollingCorrelationNode,
|
||||
"RollingCorrelation",
|
||||
wc::RollingCorrelation
|
||||
);
|
||||
node_pair_indicator!(
|
||||
RollingCovarianceNode,
|
||||
"RollingCovariance",
|
||||
wc::RollingCovariance
|
||||
);
|
||||
node_pair_indicator!(OuHalfLifeNode, "OuHalfLife", wc::OuHalfLife);
|
||||
node_pair_indicator!(SpreadHurstNode, "SpreadHurst", wc::SpreadHurst);
|
||||
node_pair_indicator!(DistanceSsdNode, "DistanceSsd", wc::DistanceSsd);
|
||||
node_pair_indicator!(
|
||||
BetaNeutralSpreadNode,
|
||||
"BetaNeutralSpread",
|
||||
wc::BetaNeutralSpread
|
||||
);
|
||||
|
||||
// ============================== PairSpreadZScore ==============================
|
||||
|
||||
@@ -581,6 +599,252 @@ impl RelativeStrengthAbNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VarianceRatio ==============================
|
||||
|
||||
/// Lo–MacKinlay variance ratio: two ctor params (`period`, `q`), one `(a, b)`
|
||||
/// pair per update, a single ratio out.
|
||||
#[napi(js_name = "VarianceRatio")]
|
||||
pub struct VarianceRatioNode {
|
||||
inner: wc::VarianceRatio,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VarianceRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, q: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VarianceRatio::new(period as usize, q as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized arrays. Returns a length-`n` array with
|
||||
/// `NaN` for warmup positions.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, a: Vec<f64>, b: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if a.len() != b.len() {
|
||||
return Err(NapiError::new(
|
||||
Status::InvalidArg,
|
||||
"a and b must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(a.len());
|
||||
for i in 0..a.len() {
|
||||
out.push(self.inner.update((a[i], b[i])).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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GrangerCausality ==============================
|
||||
|
||||
/// Granger causality F-statistic: two ctor params (`period`, `lag`), one
|
||||
/// `(a, b)` pair per update, a single F-statistic out.
|
||||
#[napi(js_name = "GrangerCausality")]
|
||||
pub struct GrangerCausalityNode {
|
||||
inner: wc::GrangerCausality,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl GrangerCausalityNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, lag: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GrangerCausality::new(period as usize, lag as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized arrays. Returns a length-`n` array with
|
||||
/// `NaN` for warmup positions.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, a: Vec<f64>, b: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if a.len() != b.len() {
|
||||
return Err(NapiError::new(
|
||||
Status::InvalidArg,
|
||||
"a and b must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(a.len());
|
||||
for i in 0..a.len() {
|
||||
out.push(self.inner.update((a[i], b[i])).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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== KalmanHedgeRatio ==============================
|
||||
|
||||
/// Kalman hedge-ratio result: dynamic hedge ratio, intercept, and spread.
|
||||
#[napi(object)]
|
||||
pub struct KalmanHedgeRatioValue {
|
||||
/// Current hedge ratio (filtered slope of `a` on `b`).
|
||||
pub hedge_ratio: f64,
|
||||
/// Current intercept (filtered level offset).
|
||||
pub intercept: f64,
|
||||
/// Forecast error `a - (intercept + hedgeRatio*b)` — the spread signal.
|
||||
pub spread: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "KalmanHedgeRatio")]
|
||||
pub struct KalmanHedgeRatioNode {
|
||||
inner: wc::KalmanHedgeRatio,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KalmanHedgeRatioNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(delta: f64, observation_var: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KalmanHedgeRatio::new(delta, observation_var).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, a: f64, b: f64) -> Option<KalmanHedgeRatioValue> {
|
||||
self.inner.update((a, b)).map(|o| KalmanHedgeRatioValue {
|
||||
hedge_ratio: o.hedge_ratio,
|
||||
intercept: o.intercept,
|
||||
spread: o.spread,
|
||||
})
|
||||
}
|
||||
/// Batch over two equally-sized arrays. Returns a flat array of length
|
||||
/// `3 * n`, interleaved per row as `[hedgeRatio0, intercept0, spread0, ...]`.
|
||||
/// Read column `j` of row `i` as `result[i * 3 + j]`. Warmup rows are `NaN`.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, a: Vec<f64>, b: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if a.len() != b.len() {
|
||||
return Err(NapiError::new(
|
||||
Status::InvalidArg,
|
||||
"a and b must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = vec![f64::NAN; a.len() * 3];
|
||||
for i in 0..a.len() {
|
||||
if let Some(o) = self.inner.update((a[i], b[i])) {
|
||||
out[i * 3] = o.hedge_ratio;
|
||||
out[i * 3 + 1] = o.intercept;
|
||||
out[i * 3 + 2] = o.spread;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== SpreadBollingerBands ==============================
|
||||
|
||||
/// Spread Bollinger-bands result: middle, upper and lower bands plus `%b`.
|
||||
#[napi(object)]
|
||||
pub struct SpreadBollingerBandsValue {
|
||||
/// Middle band: the rolling mean of the spread.
|
||||
pub middle: f64,
|
||||
/// Upper band.
|
||||
pub upper: f64,
|
||||
/// Lower band.
|
||||
pub lower: f64,
|
||||
/// `%b`: where the spread sits across the band (`0` lower, `1` upper).
|
||||
pub percent_b: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "SpreadBollingerBands")]
|
||||
pub struct SpreadBollingerBandsNode {
|
||||
inner: wc::SpreadBollingerBands,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl SpreadBollingerBandsNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, num_std: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::SpreadBollingerBands::new(period as usize, num_std).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, a: f64, b: f64) -> Option<SpreadBollingerBandsValue> {
|
||||
self.inner
|
||||
.update((a, b))
|
||||
.map(|o| SpreadBollingerBandsValue {
|
||||
middle: o.middle,
|
||||
upper: o.upper,
|
||||
lower: o.lower,
|
||||
percent_b: o.percent_b,
|
||||
})
|
||||
}
|
||||
/// Batch over two equally-sized arrays. Returns a flat array of length
|
||||
/// `4 * n`, interleaved per row as `[middle0, upper0, lower0, percentB0, ...]`.
|
||||
/// Read column `j` of row `i` as `result[i * 4 + j]`. Warmup rows are `NaN`.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, a: Vec<f64>, b: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if a.len() != b.len() {
|
||||
return Err(NapiError::new(
|
||||
Status::InvalidArg,
|
||||
"a and b must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = vec![f64::NAN; a.len() * 4];
|
||||
for i in 0..a.len() {
|
||||
if let Some(o) = self.inner.update((a[i], b[i])) {
|
||||
out[i * 4] = o.middle;
|
||||
out[i * 4 + 1] = o.upper;
|
||||
out[i * 4 + 2] = o.lower;
|
||||
out[i * 4 + 3] = o.percent_b;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MACD ==============================
|
||||
|
||||
/// MACD triple: macd line, signal line, histogram.
|
||||
|
||||
@@ -159,6 +159,16 @@ from ._wickra import (
|
||||
MarketFacilitationIndex,
|
||||
EaseOfMovement,
|
||||
# Statistics
|
||||
SpreadBollingerBands,
|
||||
KalmanHedgeRatio,
|
||||
GrangerCausality,
|
||||
VarianceRatio,
|
||||
BetaNeutralSpread,
|
||||
DistanceSsd,
|
||||
SpreadHurst,
|
||||
OuHalfLife,
|
||||
RollingCovariance,
|
||||
RollingCorrelation,
|
||||
TypicalPrice,
|
||||
MedianPrice,
|
||||
WeightedClose,
|
||||
@@ -499,6 +509,16 @@ __all__ = [
|
||||
"MarketFacilitationIndex",
|
||||
"EaseOfMovement",
|
||||
# Statistics
|
||||
"SpreadBollingerBands",
|
||||
"KalmanHedgeRatio",
|
||||
"GrangerCausality",
|
||||
"VarianceRatio",
|
||||
"BetaNeutralSpread",
|
||||
"DistanceSsd",
|
||||
"SpreadHurst",
|
||||
"OuHalfLife",
|
||||
"RollingCovariance",
|
||||
"RollingCorrelation",
|
||||
"TypicalPrice",
|
||||
"MedianPrice",
|
||||
"WeightedClose",
|
||||
|
||||
+693
-1
@@ -30,7 +30,8 @@ fn map_err(e: wc::Error) -> PyErr {
|
||||
| wc::Error::InvalidOrderBook { .. }
|
||||
| wc::Error::InvalidTrade { .. }
|
||||
| wc::Error::InvalidDerivatives { .. }
|
||||
| wc::Error::InvalidCrossSection { .. } => PyValueError::new_err(e.to_string()),
|
||||
| wc::Error::InvalidCrossSection { .. }
|
||||
| wc::Error::InvalidParameter { .. } => PyValueError::new_err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12250,6 +12251,687 @@ impl PyRelativeStrengthAB {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== RollingCorrelation ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "RollingCorrelation",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyRollingCorrelation {
|
||||
inner: wc::RollingCorrelation,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRollingCorrelation {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RollingCorrelation::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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!("RollingCorrelation(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== RollingCovariance ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "RollingCovariance",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyRollingCovariance {
|
||||
inner: wc::RollingCovariance,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRollingCovariance {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RollingCovariance::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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!("RollingCovariance(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== OuHalfLife ==============================
|
||||
|
||||
#[pyclass(name = "OuHalfLife", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyOuHalfLife {
|
||||
inner: wc::OuHalfLife,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyOuHalfLife {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=60))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::OuHalfLife::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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!("OuHalfLife(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== SpreadHurst ==============================
|
||||
|
||||
#[pyclass(name = "SpreadHurst", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PySpreadHurst {
|
||||
inner: wc::SpreadHurst,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySpreadHurst {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=60))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::SpreadHurst::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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!("SpreadHurst(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== DistanceSsd ==============================
|
||||
|
||||
#[pyclass(name = "DistanceSsd", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyDistanceSsd {
|
||||
inner: wc::DistanceSsd,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDistanceSsd {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DistanceSsd::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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!("DistanceSsd(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== BetaNeutralSpread ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "BetaNeutralSpread",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyBetaNeutralSpread {
|
||||
inner: wc::BetaNeutralSpread,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBetaNeutralSpread {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::BetaNeutralSpread::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
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!("BetaNeutralSpread(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== VarianceRatio ==============================
|
||||
|
||||
#[pyclass(name = "VarianceRatio", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyVarianceRatio {
|
||||
inner: wc::VarianceRatio,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVarianceRatio {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=60, q=2))]
|
||||
fn new(period: usize, q: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VarianceRatio::new(period, q).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
#[getter]
|
||||
fn q(&self) -> usize {
|
||||
self.inner.q()
|
||||
}
|
||||
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!(
|
||||
"VarianceRatio(period={}, q={})",
|
||||
self.inner.period(),
|
||||
self.inner.q()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GrangerCausality ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "GrangerCausality",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyGrangerCausality {
|
||||
inner: wc::GrangerCausality,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGrangerCausality {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=60, lag=1))]
|
||||
fn new(period: usize, lag: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GrangerCausality::new(period, lag).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays: `a` and `b`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(xs.len());
|
||||
for i in 0..xs.len() {
|
||||
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
#[getter]
|
||||
fn lag(&self) -> usize {
|
||||
self.inner.lag()
|
||||
}
|
||||
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!(
|
||||
"GrangerCausality(period={}, lag={})",
|
||||
self.inner.period(),
|
||||
self.inner.lag()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== KalmanHedgeRatio ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "KalmanHedgeRatio",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyKalmanHedgeRatio {
|
||||
inner: wc::KalmanHedgeRatio,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKalmanHedgeRatio {
|
||||
#[new]
|
||||
#[pyo3(signature = (delta=1e-4, observation_var=1e-3))]
|
||||
fn new(delta: f64, observation_var: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KalmanHedgeRatio::new(delta, observation_var).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(hedge_ratio, intercept, spread)` or `None` during warmup.
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<(f64, f64, f64)> {
|
||||
self.inner
|
||||
.update((a, b))
|
||||
.map(|o| (o.hedge_ratio, o.intercept, o.spread))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays. Returns a 2D array of shape
|
||||
/// `(n, 3)` with columns `[hedge_ratio, intercept, spread]`. Warmup rows are
|
||||
/// NaN.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let n = xs.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update((xs[i], ys[i])) {
|
||||
out[i * 3] = o.hedge_ratio;
|
||||
out[i * 3 + 1] = o.intercept;
|
||||
out[i * 3 + 2] = o.spread;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn delta(&self) -> f64 {
|
||||
self.inner.delta()
|
||||
}
|
||||
#[getter]
|
||||
fn observation_var(&self) -> f64 {
|
||||
self.inner.observation_var()
|
||||
}
|
||||
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!(
|
||||
"KalmanHedgeRatio(delta={}, observation_var={})",
|
||||
self.inner.delta(),
|
||||
self.inner.observation_var()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== SpreadBollingerBands ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "SpreadBollingerBands",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PySpreadBollingerBands {
|
||||
inner: wc::SpreadBollingerBands,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySpreadBollingerBands {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, num_std=2.0))]
|
||||
fn new(period: usize, num_std: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::SpreadBollingerBands::new(period, num_std).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(middle, upper, lower, percent_b)` or `None` during warmup.
|
||||
fn update(&mut self, a: f64, b: f64) -> Option<(f64, f64, f64, f64)> {
|
||||
self.inner
|
||||
.update((a, b))
|
||||
.map(|o| (o.middle, o.upper, o.lower, o.percent_b))
|
||||
}
|
||||
/// Batch over two equally-sized numpy arrays. Returns a 2D array of shape
|
||||
/// `(n, 4)` with columns `[middle, upper, lower, percent_b]`. Warmup rows are
|
||||
/// NaN.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
a: PyReadonlyArray1<'py, f64>,
|
||||
b: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let xs = a
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let ys = b
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if xs.len() != ys.len() {
|
||||
return Err(PyValueError::new_err("a and b must be equal length"));
|
||||
}
|
||||
let n = xs.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update((xs[i], ys[i])) {
|
||||
out[i * 4] = o.middle;
|
||||
out[i * 4 + 1] = o.upper;
|
||||
out[i * 4 + 2] = o.lower;
|
||||
out[i * 4 + 3] = o.percent_b;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
#[getter]
|
||||
fn num_std(&self) -> f64 {
|
||||
self.inner.num_std()
|
||||
}
|
||||
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!(
|
||||
"SpreadBollingerBands(period={}, num_std={})",
|
||||
self.inner.period(),
|
||||
self.inner.num_std()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== SpearmanCorrelation ==============================
|
||||
|
||||
#[pyclass(
|
||||
@@ -15772,6 +16454,16 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyLeadLagCrossCorrelation>()?;
|
||||
m.add_class::<PyCointegration>()?;
|
||||
m.add_class::<PyRelativeStrengthAB>()?;
|
||||
m.add_class::<PyRollingCorrelation>()?;
|
||||
m.add_class::<PyRollingCovariance>()?;
|
||||
m.add_class::<PyOuHalfLife>()?;
|
||||
m.add_class::<PySpreadHurst>()?;
|
||||
m.add_class::<PyDistanceSsd>()?;
|
||||
m.add_class::<PyBetaNeutralSpread>()?;
|
||||
m.add_class::<PyVarianceRatio>()?;
|
||||
m.add_class::<PyGrangerCausality>()?;
|
||||
m.add_class::<PyKalmanHedgeRatio>()?;
|
||||
m.add_class::<PySpreadBollingerBands>()?;
|
||||
m.add_class::<PySpearmanCorrelation>()?;
|
||||
m.add_class::<PyValueArea>()?;
|
||||
m.add_class::<PyVolumeProfile>()?;
|
||||
|
||||
@@ -167,6 +167,14 @@ def test_scalar_streaming_matches_batch(cls, args, sine_prices):
|
||||
# --- Two-series (asset, benchmark) indicators -----------------------------
|
||||
|
||||
PAIR = [
|
||||
(ta.GrangerCausality, (60, 1)),
|
||||
(ta.VarianceRatio, (60, 2)),
|
||||
(ta.BetaNeutralSpread, (20,)),
|
||||
(ta.DistanceSsd, (20,)),
|
||||
(ta.SpreadHurst, (60,)),
|
||||
(ta.OuHalfLife, (60,)),
|
||||
(ta.RollingCovariance, (20,)),
|
||||
(ta.RollingCorrelation, (20,)),
|
||||
(ta.TreynorRatio, (20, 0.0)),
|
||||
(ta.InformationRatio, (20,)),
|
||||
(ta.Alpha, (20, 0.0)),
|
||||
@@ -251,6 +259,42 @@ def test_cointegration_streaming_matches_batch():
|
||||
assert math.isclose(batch[i, 2], adf, rel_tol=1e-12, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_kalman_hedge_ratio_converges_and_streaming_matches_batch():
|
||||
n = 500
|
||||
b = np.array([100.0 + 95.0 * math.sin(t * 0.5) for t in range(n)])
|
||||
a = 2.0 * b + 5.0 # a = 2*b + 5 with a wide-ranging b ⇒ identifiable
|
||||
batch = ta.KalmanHedgeRatio(1e-2, 1e-3).batch(a, b)
|
||||
assert batch.shape == (n, 3)
|
||||
assert abs(batch[-1, 0] - 2.0) < 0.05 # hedge ratio
|
||||
assert abs(batch[-1, 2]) < 0.05 # spread (forecast error)
|
||||
streamer = ta.KalmanHedgeRatio(1e-2, 1e-3)
|
||||
for i in range(n):
|
||||
hr, ic, sp = streamer.update(float(a[i]), float(b[i]))
|
||||
assert math.isclose(batch[i, 0], hr, rel_tol=1e-12, abs_tol=1e-12)
|
||||
assert math.isclose(batch[i, 1], ic, rel_tol=1e-12, abs_tol=1e-12)
|
||||
assert math.isclose(batch[i, 2], sp, rel_tol=1e-12, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_spread_bollinger_bands_streaming_matches_batch():
|
||||
n = 60
|
||||
b = np.array([100.0 + t for t in range(n)])
|
||||
a = b + 3.0 * np.sin(np.arange(n) * 0.4)
|
||||
batch = ta.SpreadBollingerBands(20, 2.0).batch(a, b)
|
||||
assert batch.shape == (n, 4)
|
||||
streamer = ta.SpreadBollingerBands(20, 2.0)
|
||||
for i in range(n):
|
||||
v = streamer.update(float(a[i]), float(b[i]))
|
||||
if v is None:
|
||||
assert np.all(np.isnan(batch[i]))
|
||||
else:
|
||||
mid, up, lo, pct_b = v
|
||||
assert math.isclose(batch[i, 0], mid, rel_tol=1e-12, abs_tol=1e-12)
|
||||
assert math.isclose(batch[i, 1], up, rel_tol=1e-12, abs_tol=1e-12)
|
||||
assert math.isclose(batch[i, 2], lo, rel_tol=1e-12, abs_tol=1e-12)
|
||||
assert math.isclose(batch[i, 3], pct_b, rel_tol=1e-12, abs_tol=1e-12)
|
||||
assert lo <= mid <= up
|
||||
|
||||
|
||||
def test_relative_strength_constant_ratio():
|
||||
n = 30
|
||||
a = np.full(n, 200.0)
|
||||
@@ -2262,6 +2306,54 @@ def test_concealing_baby_swallow_reference():
|
||||
assert t.update((11.0, 13.0, 9.9, 10.0, 1.0, 2)) == pytest.approx(0.0)
|
||||
assert t.update((14.0, 14.1, 8.9, 9.0, 1.0, 3)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_rolling_correlation_reference():
|
||||
t = ta.RollingCorrelation(20)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_rolling_covariance_reference():
|
||||
t = ta.RollingCovariance(20)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_ou_half_life_reference():
|
||||
t = ta.OuHalfLife(60)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_spread_hurst_reference():
|
||||
t = ta.SpreadHurst(60)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_distance_ssd_reference():
|
||||
t = ta.DistanceSsd(20)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_beta_neutral_spread_reference():
|
||||
t = ta.BetaNeutralSpread(20)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_variance_ratio_reference():
|
||||
t = ta.VarianceRatio(60, 2)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
|
||||
def test_granger_causality_reference():
|
||||
t = ta.GrangerCausality(60, 1)
|
||||
assert t.update(1.0, 1.0) is None
|
||||
assert t.update(2.0, 1.5) is None
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -531,6 +531,24 @@ wasm_pair_indicator!(
|
||||
"SpearmanCorrelation",
|
||||
wc::SpearmanCorrelation
|
||||
);
|
||||
wasm_pair_indicator!(
|
||||
WasmRollingCorrelation,
|
||||
"RollingCorrelation",
|
||||
wc::RollingCorrelation
|
||||
);
|
||||
wasm_pair_indicator!(
|
||||
WasmRollingCovariance,
|
||||
"RollingCovariance",
|
||||
wc::RollingCovariance
|
||||
);
|
||||
wasm_pair_indicator!(WasmOuHalfLife, "OuHalfLife", wc::OuHalfLife);
|
||||
wasm_pair_indicator!(WasmSpreadHurst, "SpreadHurst", wc::SpreadHurst);
|
||||
wasm_pair_indicator!(WasmDistanceSsd, "DistanceSsd", wc::DistanceSsd);
|
||||
wasm_pair_indicator!(
|
||||
WasmBetaNeutralSpread,
|
||||
"BetaNeutralSpread",
|
||||
wc::BetaNeutralSpread
|
||||
);
|
||||
|
||||
// ---------- PairSpreadZScore (two params) ----------
|
||||
|
||||
@@ -748,6 +766,210 @@ impl WasmRelativeStrengthAb {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- VarianceRatio (two params) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = "VarianceRatio")]
|
||||
pub struct WasmVarianceRatio {
|
||||
inner: wc::VarianceRatio,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = "VarianceRatio")]
|
||||
impl WasmVarianceRatio {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, q: usize) -> Result<WasmVarianceRatio, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VarianceRatio::new(period, q).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized arrays of prices. Returns one `f64` per
|
||||
/// input position (`NaN` during warmup).
|
||||
pub fn batch(&mut self, a: &[f64], b: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if a.len() != b.len() {
|
||||
return Err(JsError::new("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(a.len());
|
||||
for i in 0..a.len() {
|
||||
out.push(self.inner.update((a[i], b[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- GrangerCausality (two params) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = "GrangerCausality")]
|
||||
pub struct WasmGrangerCausality {
|
||||
inner: wc::GrangerCausality,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = "GrangerCausality")]
|
||||
impl WasmGrangerCausality {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, lag: usize) -> Result<WasmGrangerCausality, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::GrangerCausality::new(period, lag).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, a: f64, b: f64) -> Option<f64> {
|
||||
self.inner.update((a, b))
|
||||
}
|
||||
/// Batch over two equally-sized arrays of prices. Returns one `f64` per
|
||||
/// input position (`NaN` during warmup).
|
||||
pub fn batch(&mut self, a: &[f64], b: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if a.len() != b.len() {
|
||||
return Err(JsError::new("a and b must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(a.len());
|
||||
for i in 0..a.len() {
|
||||
out.push(self.inner.update((a[i], b[i])).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- KalmanHedgeRatio (two params, object output) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = "KalmanHedgeRatio")]
|
||||
pub struct WasmKalmanHedgeRatio {
|
||||
inner: wc::KalmanHedgeRatio,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = "KalmanHedgeRatio")]
|
||||
impl WasmKalmanHedgeRatio {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(delta: f64, observation_var: f64) -> Result<WasmKalmanHedgeRatio, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::KalmanHedgeRatio::new(delta, observation_var).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ hedgeRatio, intercept, spread }`, or `null` during warmup.
|
||||
pub fn update(&mut self, a: f64, b: f64) -> JsValue {
|
||||
match self.inner.update((a, b)) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"hedgeRatio".into(), &o.hedge_ratio.into()).ok();
|
||||
Reflect::set(&obj, &"intercept".into(), &o.intercept.into()).ok();
|
||||
Reflect::set(&obj, &"spread".into(), &o.spread.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
/// Flat `Float64Array` of length `3 * n`:
|
||||
/// `[hedgeRatio0, intercept0, spread0, hedgeRatio1, ...]`. Warmup rows are NaN.
|
||||
pub fn batch(&mut self, a: &[f64], b: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if a.len() != b.len() {
|
||||
return Err(JsError::new("a and b must be equal length"));
|
||||
}
|
||||
let n = a.len();
|
||||
let mut out = vec![f64::NAN; n * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update((a[i], b[i])) {
|
||||
out[i * 3] = o.hedge_ratio;
|
||||
out[i * 3 + 1] = o.intercept;
|
||||
out[i * 3 + 2] = o.spread;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- SpreadBollingerBands (two params, object output) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = "SpreadBollingerBands")]
|
||||
pub struct WasmSpreadBollingerBands {
|
||||
inner: wc::SpreadBollingerBands,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = "SpreadBollingerBands")]
|
||||
impl WasmSpreadBollingerBands {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, num_std: f64) -> Result<WasmSpreadBollingerBands, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::SpreadBollingerBands::new(period, num_std).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ middle, upper, lower, percentB }`, or `null` during warmup.
|
||||
pub fn update(&mut self, a: f64, b: f64) -> JsValue {
|
||||
match self.inner.update((a, b)) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"middle".into(), &o.middle.into()).ok();
|
||||
Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok();
|
||||
Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok();
|
||||
Reflect::set(&obj, &"percentB".into(), &o.percent_b.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
/// Flat `Float64Array` of length `4 * n`:
|
||||
/// `[middle0, upper0, lower0, percentB0, middle1, ...]`. Warmup rows are NaN.
|
||||
pub fn batch(&mut self, a: &[f64], b: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if a.len() != b.len() {
|
||||
return Err(JsError::new("a and b must be equal length"));
|
||||
}
|
||||
let n = a.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update((a[i], b[i])) {
|
||||
out[i * 4] = o.middle;
|
||||
out[i * 4 + 1] = o.upper;
|
||||
out[i * 4 + 2] = o.lower;
|
||||
out[i * 4 + 3] = o.percent_b;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- KAMA (three params) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = KAMA)]
|
||||
|
||||
Reference in New Issue
Block a user