feat(seasonality): add the Seasonality & Session family (12 indicators) (#161)

## Summary

Adds the **Seasonality & Session** family — the first family that reads the wall-clock fields of `Candle::timestamp`. A new private `calendar` module decomposes an epoch-millisecond instant (shifted by a per-indicator `utc_offset_minutes`) into civil fields via Howard Hinnant's branch-light `civil_from_days` algorithm. Session / day / month rollovers are detected automatically, so callers never have to invoke `reset()` at a boundary.

Indicator counter **339 → 351**; family count **20 → 21**.

## Indicators

| Shape | Indicators |
|-------|-----------|
| Scalar (`f64`) | `SessionVwap`, `AverageDailyRange`, `OvernightGap`, `TurnOfMonth`, `SeasonalZScore` |
| Struct | `SessionHighLow`, `SessionRange` (Asia/EU/US), `OvernightIntradayReturn` |
| Profile (`Vec<f64>`) | `TimeOfDayReturnProfile`, `DayOfWeekProfile`, `IntradayVolatilityProfile`, `VolumeByTimeProfile` |

## Bindings

The input is the **full** candle (`open, high, low, close, volume, timestamp`), not the `high/low/close` slice the value-indicator helper assumes, so the Python / Node / WASM bindings are custom full-candle implementations:

- **Python** — `update((o,h,l,c,v,ts))`; `batch(open, high, low, close, volume, timestamp)` → `PyArray1` (scalar) / `PyArray2` (struct & profile), warmup rows `NaN`.
- **Node** — `update(open, high, low, close, volume, timestamp)`; `batch(...)` → flat `Vec<f64>`; struct outputs as `#[napi(object)]` values.
- **WASM** — `update` only (multi-input precedent); profiles as `Float64Array`, structs as camelCase objects, `timestamp` as `BigInt`.

## Verification

- `wickra-core`: full per-branch unit tests, **100%** coverage target; 2852 lib tests + 334 doctests green.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: clean.
- Node: 428 tests (dedicated `seasonality.test.js` streaming-vs-batch).
- Python: full suite + dedicated `test_seasonality.py` streaming-vs-batch.
- Counter check: mod-count == counted lib block == 351.
This commit is contained in:
kingchenc
2026-06-03 20:31:32 +02:00
committed by GitHub
parent 5e96d41916
commit 3ab2d6ec2d
27 changed files with 5026 additions and 55 deletions
@@ -0,0 +1,96 @@
// Streaming-vs-batch equivalence and reference values for the Seasonality &
// Session family. These indicators consume the full candle (open, high, low,
// close, volume, timestamp), so they have a dedicated suite.
const test = require('node:test');
const assert = require('node:assert/strict');
const wickra = require('..');
const HOUR = 3_600_000;
const N = 240;
const close = Array.from({ length: N }, (_, i) => 100 + Math.sin(i * 0.3) * 5 + Math.cos(i * 0.1) * 3);
const open = close.map((c, i) => c + Math.sin(i * 0.5) * 0.5);
const high = close.map((c, i) => Math.max(open[i], c) + 1);
const low = close.map((c, i) => Math.min(open[i], c) - 1);
const volume = Array.from({ length: N }, (_, i) => 1000 + (i % 24) * 50);
const ts = Array.from({ length: N }, (_, i) => i * HOUR);
function eq(a, b) {
if (Number.isNaN(a)) return Number.isNaN(b);
return Math.abs(a - b) < 1e-9;
}
function streamScalar(ind, i) {
const v = ind.update(open[i], high[i], low[i], close[i], volume[i], ts[i]);
return v === null || v === undefined ? NaN : v;
}
function checkScalar(name, make) {
test(`${name} streaming equals batch`, () => {
const a = make();
const b = make();
const batch = b.batch(open, high, low, close, volume, ts);
for (let i = 0; i < N; i += 1) {
assert.ok(eq(streamScalar(a, i), batch[i]), `${name} row ${i}`);
}
});
}
function checkMatrix(name, make, k, pick) {
test(`${name} streaming equals batch`, () => {
const a = make();
const b = make();
const batch = b.batch(open, high, low, close, volume, ts);
for (let i = 0; i < N; i += 1) {
const out = a.update(open[i], high[i], low[i], close[i], volume[i], ts[i]);
for (let j = 0; j < k; j += 1) {
const s = out === null || out === undefined ? NaN : pick(out, j);
assert.ok(eq(s, batch[i * k + j]), `${name} row ${i} col ${j}`);
}
}
});
}
checkScalar('SessionVwap', () => new wickra.SessionVwap(0));
checkScalar('OvernightGap', () => new wickra.OvernightGap(0));
checkScalar('SeasonalZScore', () => new wickra.SeasonalZScore(0));
checkScalar('AverageDailyRange', () => new wickra.AverageDailyRange(3, 0));
checkScalar('TurnOfMonth', () => new wickra.TurnOfMonth(3, 1, 0));
checkMatrix('SessionHighLow', () => new wickra.SessionHighLow(0), 2, (o, j) => (j === 0 ? o.high : o.low));
checkMatrix('SessionRange', () => new wickra.SessionRange(0), 3, (o, j) => [o.asia, o.eu, o.us][j]);
checkMatrix(
'OvernightIntradayReturn',
() => new wickra.OvernightIntradayReturn(0),
2,
(o, j) => (j === 0 ? o.overnight : o.intraday),
);
checkMatrix('TimeOfDayReturnProfile', () => new wickra.TimeOfDayReturnProfile(24, 0), 24, (o, j) => o[j]);
checkMatrix('IntradayVolatilityProfile', () => new wickra.IntradayVolatilityProfile(12, 0), 12, (o, j) => o[j]);
checkMatrix('VolumeByTimeProfile', () => new wickra.VolumeByTimeProfile(24, 0), 24, (o, j) => o[j]);
checkMatrix('DayOfWeekProfile', () => new wickra.DayOfWeekProfile(0), 7, (o, j) => o[j]);
test('SessionVwap reference value', () => {
const vwap = new wickra.SessionVwap(0);
assert.ok(eq(vwap.update(100, 100, 100, 100, 10, 0), 100));
assert.ok(eq(vwap.update(110, 110, 110, 110, 30, HOUR), 107.5));
assert.ok(eq(vwap.update(200, 200, 200, 200, 5, 24 * HOUR), 200));
});
test('OvernightGap reference value', () => {
const gap = new wickra.OvernightGap(0);
assert.equal(gap.update(99, 101, 98, 100, 1, 0), null);
assert.ok(eq(gap.update(105, 106, 104, 105.5, 1, 24 * HOUR), 0.05));
});
test('SessionHighLow reference object', () => {
const shl = new wickra.SessionHighLow(0);
shl.update(100, 105, 99, 101, 1, 0);
const out = shl.update(101, 108, 100, 107, 1, HOUR);
assert.ok(eq(out.high, 108));
assert.ok(eq(out.low, 99));
});
test('AverageDailyRange rejects zero period', () => {
assert.throws(() => new wickra.AverageDailyRange(0, 0));
});
+131
View File
@@ -349,6 +349,19 @@ export interface PnfColumnValue {
high: number
low: number
}
export interface SessionHighLowValue {
high: number
low: number
}
export interface SessionRangeValue {
asia: number
eu: number
us: number
}
export interface OvernightIntradayReturnValue {
overnight: number
intraday: number
}
export type SmaNode = SMA
export declare class SMA {
constructor(period: number)
@@ -3557,3 +3570,121 @@ export declare class Alpha {
isReady(): boolean
warmupPeriod(): number
}
export type SessionVwapNode = SessionVwap
export declare class SessionVwap {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
utcOffsetMinutes(): number
}
export type OvernightGapNode = OvernightGap
export declare class OvernightGap {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
utcOffsetMinutes(): number
}
export type SeasonalZScoreNode = SeasonalZScore
export declare class SeasonalZScore {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
utcOffsetMinutes(): number
}
export type TimeOfDayReturnProfileNode = TimeOfDayReturnProfile
export declare class TimeOfDayReturnProfile {
constructor(buckets: number, utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
buckets(): number
utcOffsetMinutes(): number
}
export type IntradayVolatilityProfileNode = IntradayVolatilityProfile
export declare class IntradayVolatilityProfile {
constructor(buckets: number, utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
buckets(): number
utcOffsetMinutes(): number
}
export type VolumeByTimeProfileNode = VolumeByTimeProfile
export declare class VolumeByTimeProfile {
constructor(buckets: number, utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
buckets(): number
utcOffsetMinutes(): number
}
export type DayOfWeekProfileNode = DayOfWeekProfile
export declare class DayOfWeekProfile {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): Array<number> | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
utcOffsetMinutes(): number
}
export type AverageDailyRangeNode = AverageDailyRange
export declare class AverageDailyRange {
constructor(period: number, utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TurnOfMonthNode = TurnOfMonth
export declare class TurnOfMonth {
constructor(nFirst: number, nLast: number, utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type SessionHighLowNode = SessionHighLow
export declare class SessionHighLow {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): SessionHighLowValue | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type SessionRangeNode = SessionRange
export declare class SessionRange {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): SessionRangeValue | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type OvernightIntradayReturnNode = OvernightIntradayReturn
export declare class OvernightIntradayReturn {
constructor(utcOffsetMinutes: number)
update(open: number, high: number, low: number, close: number, volume: number, timestamp: number): OvernightIntradayReturnValue | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>, volume: Array<number>, timestamp: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
+13 -1
View File
@@ -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, 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, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, 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, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn } = nativeBinding
module.exports.version = version
module.exports.SMA = SMA
@@ -652,3 +652,15 @@ module.exports.RenkoBars = RenkoBars
module.exports.KagiBars = KagiBars
module.exports.PointAndFigureBars = PointAndFigureBars
module.exports.Alpha = Alpha
module.exports.SessionVwap = SessionVwap
module.exports.OvernightGap = OvernightGap
module.exports.SeasonalZScore = SeasonalZScore
module.exports.TimeOfDayReturnProfile = TimeOfDayReturnProfile
module.exports.IntradayVolatilityProfile = IntradayVolatilityProfile
module.exports.VolumeByTimeProfile = VolumeByTimeProfile
module.exports.DayOfWeekProfile = DayOfWeekProfile
module.exports.AverageDailyRange = AverageDailyRange
module.exports.TurnOfMonth = TurnOfMonth
module.exports.SessionHighLow = SessionHighLow
module.exports.SessionRange = SessionRange
module.exports.OvernightIntradayReturn = OvernightIntradayReturn
+612
View File
@@ -13398,3 +13398,615 @@ impl AlphaNode {
self.inner.warmup_period() as u32
}
}
// ====================== Seasonality & Session (full-candle) ======================
//
// These read the wall-clock fields of `Candle::timestamp`, so the bindings take
// the full candle (open, high, low, close, volume, timestamp) rather than the
// high/low/close slice used by the candle indicators above.
fn season_candles(
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
timestamp: &[i64],
) -> napi::Result<Vec<wc::Candle>> {
let n = open.len();
if [
high.len(),
low.len(),
close.len(),
volume.len(),
timestamp.len(),
]
.iter()
.any(|&x| x != n)
{
return Err(NapiError::from_reason(
"open, high, low, close, volume, timestamp must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(n);
for i in 0..n {
out.push(
wc::Candle::new(open[i], high[i], low[i], close[i], volume[i], timestamp[i])
.map_err(map_err)?,
);
}
Ok(out)
}
macro_rules! node_seasonality_offset_scalar {
($wrapper:ident, $node_name:literal, $rust_ty:ty) => {
#[napi(js_name = $node_name)]
pub struct $wrapper {
inner: $rust_ty,
}
#[napi]
impl $wrapper {
#[napi(constructor)]
pub fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: <$rust_ty>::new(utc_offset_minutes),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<f64>> {
Ok(self.inner.update(
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?,
))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
Ok(candles
.into_iter()
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
.collect())
}
#[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
}
#[napi(js_name = "utcOffsetMinutes")]
pub fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
}
};
}
macro_rules! node_seasonality_bucket_profile {
($wrapper:ident, $node_name:literal, $rust_ty:ty) => {
#[napi(js_name = $node_name)]
pub struct $wrapper {
inner: $rust_ty,
}
#[napi]
impl $wrapper {
#[napi(constructor)]
pub fn new(buckets: u32, utc_offset_minutes: i32) -> napi::Result<Self> {
Ok(Self {
inner: <$rust_ty>::new(buckets as usize, utc_offset_minutes)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<Vec<f64>>> {
Ok(self
.inner
.update(
wc::Candle::new(open, high, low, close, volume, timestamp)
.map_err(map_err)?,
)
.map(|o| o.bins))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let k = self.inner.params().0;
let n = candles.len();
let mut out = vec![f64::NAN; n * k];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
for (j, b) in o.bins.iter().enumerate() {
out[i * k + j] = *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
}
#[napi(js_name = "buckets")]
pub fn buckets(&self) -> u32 {
self.inner.params().0 as u32
}
#[napi(js_name = "utcOffsetMinutes")]
pub fn utc_offset_minutes(&self) -> i32 {
self.inner.params().1
}
}
};
}
macro_rules! node_seasonality_offset_profile {
($wrapper:ident, $node_name:literal, $rust_ty:ty, $k:expr) => {
#[napi(js_name = $node_name)]
pub struct $wrapper {
inner: $rust_ty,
}
#[napi]
impl $wrapper {
#[napi(constructor)]
pub fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: <$rust_ty>::new(utc_offset_minutes),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<Vec<f64>>> {
Ok(self
.inner
.update(
wc::Candle::new(open, high, low, close, volume, timestamp)
.map_err(map_err)?,
)
.map(|o| o.bins))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let k = $k;
let n = candles.len();
let mut out = vec![f64::NAN; n * k];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
for (j, b) in o.bins.iter().enumerate() {
out[i * k + j] = *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
}
#[napi(js_name = "utcOffsetMinutes")]
pub fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
}
};
}
node_seasonality_offset_scalar!(SessionVwapNode, "SessionVwap", wc::SessionVwap);
node_seasonality_offset_scalar!(OvernightGapNode, "OvernightGap", wc::OvernightGap);
node_seasonality_offset_scalar!(SeasonalZScoreNode, "SeasonalZScore", wc::SeasonalZScore);
node_seasonality_bucket_profile!(
TimeOfDayReturnProfileNode,
"TimeOfDayReturnProfile",
wc::TimeOfDayReturnProfile
);
node_seasonality_bucket_profile!(
IntradayVolatilityProfileNode,
"IntradayVolatilityProfile",
wc::IntradayVolatilityProfile
);
node_seasonality_bucket_profile!(
VolumeByTimeProfileNode,
"VolumeByTimeProfile",
wc::VolumeByTimeProfile
);
node_seasonality_offset_profile!(
DayOfWeekProfileNode,
"DayOfWeekProfile",
wc::DayOfWeekProfile,
7
);
#[napi(js_name = "AverageDailyRange")]
pub struct AverageDailyRangeNode {
inner: wc::AverageDailyRange,
}
#[napi]
impl AverageDailyRangeNode {
#[napi(constructor)]
pub fn new(period: u32, utc_offset_minutes: i32) -> napi::Result<Self> {
Ok(Self {
inner: wc::AverageDailyRange::new(period as usize, utc_offset_minutes)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<f64>> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
Ok(candles
.into_iter()
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
.collect())
}
#[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
}
}
#[napi(js_name = "TurnOfMonth")]
pub struct TurnOfMonthNode {
inner: wc::TurnOfMonth,
}
#[napi]
impl TurnOfMonthNode {
#[napi(constructor)]
pub fn new(n_first: u32, n_last: u32, utc_offset_minutes: i32) -> napi::Result<Self> {
Ok(Self {
inner: wc::TurnOfMonth::new(n_first, n_last, utc_offset_minutes).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<f64>> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
Ok(candles
.into_iter()
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
.collect())
}
#[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
}
}
#[napi(object)]
pub struct SessionHighLowValue {
pub high: f64,
pub low: f64,
}
#[napi(js_name = "SessionHighLow")]
pub struct SessionHighLowNode {
inner: wc::SessionHighLow,
}
#[napi]
impl SessionHighLowNode {
#[napi(constructor)]
pub fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::SessionHighLow::new(utc_offset_minutes),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<SessionHighLowValue>> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?)
.map(|o| SessionHighLowValue {
high: o.high,
low: o.low,
}))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let n = candles.len();
let mut out = vec![f64::NAN; n * 2];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.high;
out[i * 2 + 1] = o.low;
}
}
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
}
}
#[napi(object)]
pub struct SessionRangeValue {
pub asia: f64,
pub eu: f64,
pub us: f64,
}
#[napi(js_name = "SessionRange")]
pub struct SessionRangeNode {
inner: wc::SessionRange,
}
#[napi]
impl SessionRangeNode {
#[napi(constructor)]
pub fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::SessionRange::new(utc_offset_minutes),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<SessionRangeValue>> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?)
.map(|o| SessionRangeValue {
asia: o.asia,
eu: o.eu,
us: o.us,
}))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let n = candles.len();
let mut out = vec![f64::NAN; n * 3];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
out[i * 3] = o.asia;
out[i * 3 + 1] = o.eu;
out[i * 3 + 2] = o.us;
}
}
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
}
}
#[napi(object)]
pub struct OvernightIntradayReturnValue {
pub overnight: f64,
pub intraday: f64,
}
#[napi(js_name = "OvernightIntradayReturn")]
pub struct OvernightIntradayReturnNode {
inner: wc::OvernightIntradayReturn,
}
#[napi]
impl OvernightIntradayReturnNode {
#[napi(constructor)]
pub fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::OvernightIntradayReturn::new(utc_offset_minutes),
}
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> napi::Result<Option<OvernightIntradayReturnValue>> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?)
.map(|o| OvernightIntradayReturnValue {
overnight: o.overnight,
intraday: o.intraday,
}))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
timestamp: Vec<i64>,
) -> napi::Result<Vec<f64>> {
let candles = season_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let n = candles.len();
let mut out = vec![f64::NAN; n * 2];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.overnight;
out[i * 2 + 1] = o.intraday;
}
}
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
}
}
+26
View File
@@ -385,6 +385,19 @@ from ._wickra import (
TreynorRatio,
InformationRatio,
Alpha,
# Seasonality & Session
SessionVwap,
SessionHighLow,
SessionRange,
AverageDailyRange,
OvernightGap,
OvernightIntradayReturn,
TurnOfMonth,
SeasonalZScore,
TimeOfDayReturnProfile,
DayOfWeekProfile,
IntradayVolatilityProfile,
VolumeByTimeProfile,
)
__all__ = [
@@ -749,4 +762,17 @@ __all__ = [
"TreynorRatio",
"InformationRatio",
"Alpha",
# Seasonality & Session
"SessionVwap",
"SessionHighLow",
"SessionRange",
"AverageDailyRange",
"OvernightGap",
"OvernightIntradayReturn",
"TurnOfMonth",
"SeasonalZScore",
"TimeOfDayReturnProfile",
"DayOfWeekProfile",
"IntradayVolatilityProfile",
"VolumeByTimeProfile",
]
+600
View File
@@ -17243,6 +17243,593 @@ impl PyPointAndFigureBars {
// ============================== Module ==============================
// ====================== Seasonality & Session (full-candle) ======================
//
// These indicators read the wall-clock fields of `Candle::timestamp`, so the
// bindings consume the FULL candle (open, high, low, close, volume, timestamp)
// — unlike the high/low/close candle indicators above.
fn build_seasonality_candles<'py>(
open: &PyReadonlyArray1<'py, f64>,
high: &PyReadonlyArray1<'py, f64>,
low: &PyReadonlyArray1<'py, f64>,
close: &PyReadonlyArray1<'py, f64>,
volume: &PyReadonlyArray1<'py, f64>,
timestamp: &PyReadonlyArray1<'py, i64>,
) -> PyResult<Vec<wc::Candle>> {
let o = open
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let v = volume
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let t = timestamp
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let n = o.len();
if [h.len(), l.len(), c.len(), v.len(), t.len()]
.iter()
.any(|&x| x != n)
{
return Err(PyValueError::new_err(
"open, high, low, close, volume, timestamp must be equal length",
));
}
let mut candles = Vec::with_capacity(n);
for i in 0..n {
candles.push(wc::Candle::new(o[i], h[i], l[i], c[i], v[i], t[i]).map_err(map_err)?);
}
Ok(candles)
}
macro_rules! py_seasonality_offset_scalar {
($pytype:ident, $name:literal, $rust:ident) => {
#[pyclass(name = $name, module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct $pytype {
inner: wc::$rust,
}
#[pymethods]
impl $pytype {
#[new]
#[pyo3(signature = (utc_offset_minutes = 0))]
fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::$rust::new(utc_offset_minutes),
}
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
Ok(self.inner.update(extract_candle(candle)?))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let candles =
build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let out: Vec<f64> = candles
.into_iter()
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
.collect();
Ok(out.into_pyarray(py))
}
#[getter]
fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
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!(
"{}(utc_offset_minutes={})",
$name,
self.inner.utc_offset_minutes()
)
}
}
};
}
macro_rules! py_seasonality_bucket_profile {
($pytype:ident, $name:literal, $rust:ident) => {
#[pyclass(name = $name, module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct $pytype {
inner: wc::$rust,
}
#[pymethods]
impl $pytype {
#[new]
#[pyo3(signature = (buckets = 24, utc_offset_minutes = 0))]
fn new(buckets: usize, utc_offset_minutes: i32) -> PyResult<Self> {
Ok(Self {
inner: wc::$rust::new(buckets, utc_offset_minutes).map_err(map_err)?,
})
}
fn update<'py>(
&mut self,
py: Python<'py>,
candle: &Bound<'_, PyAny>,
) -> PyResult<Option<Bound<'py, PyArray1<f64>>>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| o.bins.into_pyarray(py)))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let candles =
build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let k = self.inner.params().0;
let n = candles.len();
let mut out = vec![f64::NAN; n * k];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
for (j, b) in o.bins.iter().enumerate() {
out[i * k + j] = *b;
}
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, k), out)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn params(&self) -> (usize, i32) {
self.inner.params()
}
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 {
let (buckets, offset) = self.inner.params();
format!("{}(buckets={buckets}, utc_offset_minutes={offset})", $name)
}
}
};
}
macro_rules! py_seasonality_offset_profile {
($pytype:ident, $name:literal, $rust:ident, $k:expr) => {
#[pyclass(name = $name, module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct $pytype {
inner: wc::$rust,
}
#[pymethods]
impl $pytype {
#[new]
#[pyo3(signature = (utc_offset_minutes = 0))]
fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::$rust::new(utc_offset_minutes),
}
}
fn update<'py>(
&mut self,
py: Python<'py>,
candle: &Bound<'_, PyAny>,
) -> PyResult<Option<Bound<'py, PyArray1<f64>>>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| o.bins.into_pyarray(py)))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let candles =
build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let k = $k;
let n = candles.len();
let mut out = vec![f64::NAN; n * k];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
for (j, b) in o.bins.iter().enumerate() {
out[i * k + j] = *b;
}
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, k), out)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
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!(
"{}(utc_offset_minutes={})",
$name,
self.inner.utc_offset_minutes()
)
}
}
};
}
py_seasonality_offset_scalar!(PySessionVwap, "SessionVwap", SessionVwap);
py_seasonality_offset_scalar!(PyOvernightGap, "OvernightGap", OvernightGap);
py_seasonality_offset_scalar!(PySeasonalZScore, "SeasonalZScore", SeasonalZScore);
py_seasonality_bucket_profile!(
PyTimeOfDayReturnProfile,
"TimeOfDayReturnProfile",
TimeOfDayReturnProfile
);
py_seasonality_bucket_profile!(
PyIntradayVolatilityProfile,
"IntradayVolatilityProfile",
IntradayVolatilityProfile
);
py_seasonality_bucket_profile!(
PyVolumeByTimeProfile,
"VolumeByTimeProfile",
VolumeByTimeProfile
);
py_seasonality_offset_profile!(PyDayOfWeekProfile, "DayOfWeekProfile", DayOfWeekProfile, 7);
#[pyclass(
name = "AverageDailyRange",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyAverageDailyRange {
inner: wc::AverageDailyRange,
}
#[pymethods]
impl PyAverageDailyRange {
#[new]
#[pyo3(signature = (period = 14, utc_offset_minutes = 0))]
fn new(period: usize, utc_offset_minutes: i32) -> PyResult<Self> {
Ok(Self {
inner: wc::AverageDailyRange::new(period, utc_offset_minutes).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
Ok(self.inner.update(extract_candle(candle)?))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let out: Vec<f64> = candles
.into_iter()
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
.collect();
Ok(out.into_pyarray(py))
}
#[getter]
fn params(&self) -> (usize, i32) {
self.inner.params()
}
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 {
let (period, offset) = self.inner.params();
format!("AverageDailyRange(period={period}, utc_offset_minutes={offset})")
}
}
#[pyclass(name = "TurnOfMonth", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyTurnOfMonth {
inner: wc::TurnOfMonth,
}
#[pymethods]
impl PyTurnOfMonth {
#[new]
#[pyo3(signature = (n_first = 3, n_last = 1, utc_offset_minutes = 0))]
fn new(n_first: u32, n_last: u32, utc_offset_minutes: i32) -> PyResult<Self> {
Ok(Self {
inner: wc::TurnOfMonth::new(n_first, n_last, utc_offset_minutes).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
Ok(self.inner.update(extract_candle(candle)?))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let out: Vec<f64> = candles
.into_iter()
.map(|c| self.inner.update(c).unwrap_or(f64::NAN))
.collect();
Ok(out.into_pyarray(py))
}
#[getter]
fn params(&self) -> (u32, u32, i32) {
self.inner.params()
}
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 {
let (n_first, n_last, offset) = self.inner.params();
format!("TurnOfMonth(n_first={n_first}, n_last={n_last}, utc_offset_minutes={offset})")
}
}
#[pyclass(
name = "SessionHighLow",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PySessionHighLow {
inner: wc::SessionHighLow,
}
#[pymethods]
impl PySessionHighLow {
#[new]
#[pyo3(signature = (utc_offset_minutes = 0))]
fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::SessionHighLow::new(utc_offset_minutes),
}
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.high, o.low)))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let n = candles.len();
let mut out = vec![f64::NAN; n * 2];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.high;
out[i * 2 + 1] = o.low;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
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!(
"SessionHighLow(utc_offset_minutes={})",
self.inner.utc_offset_minutes()
)
}
}
#[pyclass(name = "SessionRange", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PySessionRange {
inner: wc::SessionRange,
}
#[pymethods]
impl PySessionRange {
#[new]
#[pyo3(signature = (utc_offset_minutes = 0))]
fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::SessionRange::new(utc_offset_minutes),
}
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.asia, o.eu, o.us)))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let n = candles.len();
let mut out = vec![f64::NAN; n * 3];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
out[i * 3] = o.asia;
out[i * 3 + 1] = o.eu;
out[i * 3 + 2] = o.us;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
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!(
"SessionRange(utc_offset_minutes={})",
self.inner.utc_offset_minutes()
)
}
}
#[pyclass(
name = "OvernightIntradayReturn",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyOvernightIntradayReturn {
inner: wc::OvernightIntradayReturn,
}
#[pymethods]
impl PyOvernightIntradayReturn {
#[new]
#[pyo3(signature = (utc_offset_minutes = 0))]
fn new(utc_offset_minutes: i32) -> Self {
Self {
inner: wc::OvernightIntradayReturn::new(utc_offset_minutes),
}
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.overnight, o.intraday)))
}
#[allow(clippy::too_many_arguments)]
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
timestamp: PyReadonlyArray1<'py, i64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let candles = build_seasonality_candles(&open, &high, &low, &close, &volume, &timestamp)?;
let n = candles.len();
let mut out = vec![f64::NAN; n * 2];
for (i, c) in candles.into_iter().enumerate() {
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.overnight;
out[i * 2 + 1] = o.intraday;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
#[getter]
fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
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!(
"OvernightIntradayReturn(utc_offset_minutes={})",
self.inner.utc_offset_minutes()
)
}
}
#[pymodule]
#[allow(clippy::too_many_lines)]
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -17596,5 +18183,18 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyRocr100>()?;
m.add_class::<PyLinRegIntercept>()?;
m.add_class::<PyTsf>()?;
// Family 16: Seasonality & Session.
m.add_class::<PySessionVwap>()?;
m.add_class::<PySessionHighLow>()?;
m.add_class::<PySessionRange>()?;
m.add_class::<PyAverageDailyRange>()?;
m.add_class::<PyOvernightGap>()?;
m.add_class::<PyOvernightIntradayReturn>()?;
m.add_class::<PyTurnOfMonth>()?;
m.add_class::<PySeasonalZScore>()?;
m.add_class::<PyTimeOfDayReturnProfile>()?;
m.add_class::<PyDayOfWeekProfile>()?;
m.add_class::<PyIntradayVolatilityProfile>()?;
m.add_class::<PyVolumeByTimeProfile>()?;
Ok(())
}
+132
View File
@@ -0,0 +1,132 @@
"""Streaming-vs-batch equivalence and reference values for the Seasonality &
Session family.
These indicators read the full candle (including ``timestamp``), so they have a
dedicated test rather than joining the timestamp-less parametrize harness in
``test_new_indicators.py``.
"""
import numpy as np
import pytest
import wickra as ta
HOUR_MS = 3_600_000
@pytest.fixture(scope="module")
def candle_columns():
"""240 hourly candles (10 days) with valid OHLCV and epoch-ms timestamps."""
n = 240
t = np.arange(n, dtype=np.float64)
close = 100.0 + np.sin(t * 0.3) * 5.0 + np.cos(t * 0.1) * 3.0
open_ = close + np.sin(t * 0.5) * 0.5
high = np.maximum(open_, close) + 1.0
low = np.minimum(open_, close) - 1.0
volume = 1000.0 + (t % 24) * 50.0
timestamp = (np.arange(n, dtype=np.int64)) * HOUR_MS
return open_, high, low, close, volume, timestamp
def _candles(cols):
open_, high, low, close, volume, timestamp = cols
return [
(open_[i], high[i], low[i], close[i], volume[i], int(timestamp[i]))
for i in range(len(close))
]
def _check_scalar(make, cols):
candles = _candles(cols)
a, b = make(), make()
stream = np.array(
[np.nan if (v := a.update(c)) is None else v for c in candles],
dtype=np.float64,
)
batch = np.asarray(b.batch(*cols))
np.testing.assert_allclose(stream, batch, equal_nan=True, rtol=1e-9, atol=1e-9)
def _check_matrix(make, k, cols):
candles = _candles(cols)
a, b = make(), make()
rows = []
for c in candles:
out = a.update(c)
rows.append(np.full(k, np.nan) if out is None else np.asarray(out, dtype=float))
stream = np.vstack(rows)
batch = np.asarray(b.batch(*cols))
assert batch.shape == (len(candles), k)
np.testing.assert_allclose(stream, batch, equal_nan=True, rtol=1e-9, atol=1e-9)
SCALAR = [
lambda: ta.SessionVwap(0),
lambda: ta.OvernightGap(0),
lambda: ta.SeasonalZScore(0),
lambda: ta.AverageDailyRange(3, 0),
lambda: ta.TurnOfMonth(3, 1, 0),
]
MATRIX = [
(lambda: ta.SessionHighLow(0), 2),
(lambda: ta.SessionRange(0), 3),
(lambda: ta.OvernightIntradayReturn(0), 2),
(lambda: ta.TimeOfDayReturnProfile(24, 0), 24),
(lambda: ta.IntradayVolatilityProfile(12, 0), 12),
(lambda: ta.VolumeByTimeProfile(24, 0), 24),
(lambda: ta.DayOfWeekProfile(0), 7),
]
@pytest.mark.parametrize("make", SCALAR)
def test_scalar_streaming_equals_batch(make, candle_columns):
_check_scalar(make, candle_columns)
@pytest.mark.parametrize("make,k", MATRIX)
def test_matrix_streaming_equals_batch(make, k, candle_columns):
_check_matrix(make, k, candle_columns)
def test_session_vwap_reference():
vwap = ta.SessionVwap(0)
# typical = close for a flat candle; volume-weighted within the day.
v1 = vwap.update((100.0, 100.0, 100.0, 100.0, 10.0, 0))
assert v1 == pytest.approx(100.0)
v2 = vwap.update((110.0, 110.0, 110.0, 110.0, 30.0, HOUR_MS))
assert v2 == pytest.approx(107.5)
# New day re-anchors.
v3 = vwap.update((200.0, 200.0, 200.0, 200.0, 5.0, 24 * HOUR_MS))
assert v3 == pytest.approx(200.0)
def test_overnight_gap_reference():
gap = ta.OvernightGap(0)
assert gap.update((99.0, 101.0, 98.0, 100.0, 1.0, 0)) is None
g = gap.update((105.0, 106.0, 104.0, 105.5, 1.0, 24 * HOUR_MS))
assert g == pytest.approx(0.05)
def test_session_high_low_reference():
shl = ta.SessionHighLow(0)
shl.update((100.0, 105.0, 99.0, 101.0, 1.0, 0))
out = shl.update((101.0, 108.0, 100.0, 107.0, 1.0, HOUR_MS))
assert out == (108.0, 99.0)
def test_volume_by_time_profile_reference():
prof = ta.VolumeByTimeProfile(24, 0)
out = prof.update((100.0, 100.0, 100.0, 100.0, 500.0, HOUR_MS)) # 01:00 -> bucket 1
assert out[1] == pytest.approx(500.0)
assert out[0] == pytest.approx(0.0)
def test_rejects_zero_buckets():
with pytest.raises(ValueError):
ta.TimeOfDayReturnProfile(0, 0)
def test_average_daily_range_rejects_zero_period():
with pytest.raises(ValueError):
ta.AverageDailyRange(0, 0)
+387
View File
@@ -10226,3 +10226,390 @@ impl WasmAlpha {
self.inner.warmup_period()
}
}
// ====================== Seasonality & Session (full-candle) ======================
//
// These read the wall-clock fields of `Candle::timestamp`. JS passes `timestamp`
// as a BigInt (epoch milliseconds). Following the multi-input precedent
// (microstructure / derivatives), WASM exposes streaming `update` only — no
// batch over ragged multi-arrays.
macro_rules! wasm_seasonality_offset_scalar {
($wrapper:ident, $js:ident, $rust:ty) => {
#[wasm_bindgen(js_name = $js)]
pub struct $wrapper {
inner: $rust,
}
#[wasm_bindgen(js_class = $js)]
impl $wrapper {
#[wasm_bindgen(constructor)]
pub fn new(utc_offset_minutes: i32) -> $wrapper {
Self {
inner: <$rust>::new(utc_offset_minutes),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<Option<f64>, JsError> {
Ok(self.inner.update(
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?,
))
}
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()
}
#[wasm_bindgen(js_name = utcOffsetMinutes)]
pub fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
}
};
}
macro_rules! wasm_seasonality_bucket_profile {
($wrapper:ident, $js:ident, $rust:ty) => {
#[wasm_bindgen(js_name = $js)]
pub struct $wrapper {
inner: $rust,
}
#[wasm_bindgen(js_class = $js)]
impl $wrapper {
#[wasm_bindgen(constructor)]
pub fn new(buckets: usize, utc_offset_minutes: i32) -> Result<$wrapper, JsError> {
Ok(Self {
inner: <$rust>::new(buckets, utc_offset_minutes).map_err(map_err)?,
})
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<JsValue, JsError> {
let c =
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
Ok(match self.inner.update(c) {
Some(o) => Float64Array::from(o.bins.as_slice()).into(),
None => JsValue::NULL,
})
}
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()
}
#[wasm_bindgen(js_name = utcOffsetMinutes)]
pub fn utc_offset_minutes(&self) -> i32 {
self.inner.params().1
}
}
};
}
macro_rules! wasm_seasonality_offset_profile {
($wrapper:ident, $js:ident, $rust:ty) => {
#[wasm_bindgen(js_name = $js)]
pub struct $wrapper {
inner: $rust,
}
#[wasm_bindgen(js_class = $js)]
impl $wrapper {
#[wasm_bindgen(constructor)]
pub fn new(utc_offset_minutes: i32) -> $wrapper {
Self {
inner: <$rust>::new(utc_offset_minutes),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<JsValue, JsError> {
let c =
wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
Ok(match self.inner.update(c) {
Some(o) => Float64Array::from(o.bins.as_slice()).into(),
None => JsValue::NULL,
})
}
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()
}
#[wasm_bindgen(js_name = utcOffsetMinutes)]
pub fn utc_offset_minutes(&self) -> i32 {
self.inner.utc_offset_minutes()
}
}
};
}
wasm_seasonality_offset_scalar!(WasmSessionVwap, SessionVwap, wc::SessionVwap);
wasm_seasonality_offset_scalar!(WasmOvernightGap, OvernightGap, wc::OvernightGap);
wasm_seasonality_offset_scalar!(WasmSeasonalZScore, SeasonalZScore, wc::SeasonalZScore);
wasm_seasonality_bucket_profile!(
WasmTimeOfDayReturnProfile,
TimeOfDayReturnProfile,
wc::TimeOfDayReturnProfile
);
wasm_seasonality_bucket_profile!(
WasmIntradayVolatilityProfile,
IntradayVolatilityProfile,
wc::IntradayVolatilityProfile
);
wasm_seasonality_bucket_profile!(
WasmVolumeByTimeProfile,
VolumeByTimeProfile,
wc::VolumeByTimeProfile
);
wasm_seasonality_offset_profile!(WasmDayOfWeekProfile, DayOfWeekProfile, wc::DayOfWeekProfile);
#[wasm_bindgen(js_name = AverageDailyRange)]
pub struct WasmAverageDailyRange {
inner: wc::AverageDailyRange,
}
#[wasm_bindgen(js_class = AverageDailyRange)]
impl WasmAverageDailyRange {
#[wasm_bindgen(constructor)]
pub fn new(period: usize, utc_offset_minutes: i32) -> Result<WasmAverageDailyRange, JsError> {
Ok(Self {
inner: wc::AverageDailyRange::new(period, utc_offset_minutes).map_err(map_err)?,
})
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
}
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()
}
}
#[wasm_bindgen(js_name = TurnOfMonth)]
pub struct WasmTurnOfMonth {
inner: wc::TurnOfMonth,
}
#[wasm_bindgen(js_class = TurnOfMonth)]
impl WasmTurnOfMonth {
#[wasm_bindgen(constructor)]
pub fn new(
n_first: u32,
n_last: u32,
utc_offset_minutes: i32,
) -> Result<WasmTurnOfMonth, JsError> {
Ok(Self {
inner: wc::TurnOfMonth::new(n_first, n_last, utc_offset_minutes).map_err(map_err)?,
})
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<Option<f64>, JsError> {
Ok(self
.inner
.update(wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?))
}
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()
}
}
#[wasm_bindgen(js_name = SessionHighLow)]
pub struct WasmSessionHighLow {
inner: wc::SessionHighLow,
}
#[wasm_bindgen(js_class = SessionHighLow)]
impl WasmSessionHighLow {
#[wasm_bindgen(constructor)]
pub fn new(utc_offset_minutes: i32) -> WasmSessionHighLow {
Self {
inner: wc::SessionHighLow::new(utc_offset_minutes),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<JsValue, JsError> {
let c = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"high".into(), &o.high.into()).ok();
Reflect::set(&obj, &"low".into(), &o.low.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
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()
}
}
#[wasm_bindgen(js_name = SessionRange)]
pub struct WasmSessionRange {
inner: wc::SessionRange,
}
#[wasm_bindgen(js_class = SessionRange)]
impl WasmSessionRange {
#[wasm_bindgen(constructor)]
pub fn new(utc_offset_minutes: i32) -> WasmSessionRange {
Self {
inner: wc::SessionRange::new(utc_offset_minutes),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<JsValue, JsError> {
let c = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"asia".into(), &o.asia.into()).ok();
Reflect::set(&obj, &"eu".into(), &o.eu.into()).ok();
Reflect::set(&obj, &"us".into(), &o.us.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
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()
}
}
#[wasm_bindgen(js_name = OvernightIntradayReturn)]
pub struct WasmOvernightIntradayReturn {
inner: wc::OvernightIntradayReturn,
}
#[wasm_bindgen(js_class = OvernightIntradayReturn)]
impl WasmOvernightIntradayReturn {
#[wasm_bindgen(constructor)]
pub fn new(utc_offset_minutes: i32) -> WasmOvernightIntradayReturn {
Self {
inner: wc::OvernightIntradayReturn::new(utc_offset_minutes),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
timestamp: i64,
) -> Result<JsValue, JsError> {
let c = wc::Candle::new(open, high, low, close, volume, timestamp).map_err(map_err)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"overnight".into(), &o.overnight.into()).ok();
Reflect::set(&obj, &"intraday".into(), &o.intraday.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
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()
}
}