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
+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
}