pond-ts 0.44.1 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -1
- package/dist/batch/time-series.d.ts +35 -5
- package/dist/batch/time-series.js +117 -15
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
|
8
8
|
tag, so this file covers them all. Pre-1.0: minor bumps may include new features
|
|
9
9
|
and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.45.0...HEAD
|
|
12
|
+
[0.45.0]: https://github.com/pjm17971/pond-ts/compare/v0.44.1...v0.45.0
|
|
12
13
|
[0.44.1]: https://github.com/pjm17971/pond-ts/compare/v0.44.0...v0.44.1
|
|
13
14
|
[0.44.0]: https://github.com/pjm17971/pond-ts/compare/v0.43.0...v0.44.0
|
|
14
15
|
[0.43.0]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...v0.43.0
|
|
@@ -43,6 +44,98 @@ and type-level changes; patch bumps are strictly additive.
|
|
|
43
44
|
|
|
44
45
|
## [Unreleased]
|
|
45
46
|
|
|
47
|
+
## [0.45.0] — 2026-07-14
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **charts:** the time axis now walks a **logical tick ladder** — clock/calendar
|
|
52
|
+
units (1s…30s, 1m…30m, 1H…12H, day / week / month / quarter / year), picking
|
|
53
|
+
the finest grain that fits the width-derived cap — and renders **two-tier labels**:
|
|
54
|
+
a first row at the tick grain (`14:00`, `Feb 02`, `Feb`, `2026`) plus a
|
|
55
|
+
**boundary row** carrying the coarser context the first row omits (the date
|
|
56
|
+
under clock ticks, the year under day/week/month ticks — never a unit the
|
|
57
|
+
first row already shows), shown once under
|
|
58
|
+
the first tick of each new period. Hour anchors are generated in **live**
|
|
59
|
+
trading time, so none lands in a collapsed gap, an early close, or a lunch
|
|
60
|
+
break. `TradingTimeScale` gains `tickBoundaries(count)`; a cramped leading
|
|
61
|
+
partial-period anchor (the `"Jun 23Jul 07"` pile-up) is dropped. Systematic
|
|
62
|
+
story matrix under `Charts/TimeAxisTicks`.
|
|
63
|
+
- **financial:** the rest of the first study batch — `rollingStdev`,
|
|
64
|
+
`rollingMin`, `rollingMax`, `rollingPercentile` (linear interpolation),
|
|
65
|
+
`zScore` (rolling), `envelope` (MA ± percent, `maType` sma/ema), and
|
|
66
|
+
`percentChange` (n-bar rate of change). Same shape as the others — `column` /
|
|
67
|
+
`output` (or `prefix`) on every one, bar-count periods, length-preserving
|
|
68
|
+
warm-up — and each has a fluent method (`bars.zScore({ period: 20 })`) and a
|
|
69
|
+
**pandas oracle** case (values cross-validated bar-for-bar). This completes the
|
|
70
|
+
#449 first batch.
|
|
71
|
+
- **financial:** opt-in **fluent studies** via `import '@pond-ts/financial/fluent'`
|
|
72
|
+
— mounts `sma` / `ema` / `bollinger` as chainable `TimeSeries` methods so
|
|
73
|
+
composition reads like the core operators beside it:
|
|
74
|
+
`bars.sma({ period: 20 }).ema({ period: 12 }).bollinger({ period: 20 })`.
|
|
75
|
+
Opt-in by import (the default entry leaves `TimeSeries` untouched, so a
|
|
76
|
+
non-financial series never sees `.sma()`); the methods are exactly the
|
|
77
|
+
standalone functions bound to `this`, fully typed (each appends its column to
|
|
78
|
+
the schema). Same prototype-augmentation pattern core uses for column methods.
|
|
79
|
+
- **financial:** `@pond-ts/financial` gains its first **studies** — `sma`, `ema`,
|
|
80
|
+
and `bollinger` (Bollinger Bands®) — pure functions that append a column (or a
|
|
81
|
+
`${prefix}Middle/Upper/Lower` family) to a bar series. Every study takes a
|
|
82
|
+
`column` source (default `'close'`) and an `output` name, so a study runs over
|
|
83
|
+
any numeric column including another study's output; periods are **bar counts**
|
|
84
|
+
(on core's count window, gap-correct); warm-up rows are `undefined`
|
|
85
|
+
(length-preserving). Plus the `OhlcvColumns` contract + `DEFAULT_OHLCV`. (Core
|
|
86
|
+
`AppendColumn` is now exported so study return types name their appended
|
|
87
|
+
column.)
|
|
88
|
+
- **core:** `smooth(col, 'ema', …)` gains the financial **`span`** rate
|
|
89
|
+
convention (`α = 2/(span+1)`, e.g. `{ span: 12 }` for a 12-period EMA) as an
|
|
90
|
+
alternative to `alpha` (exactly one required), and a length-preserving
|
|
91
|
+
**`minSamples`** warm-up that emits `undefined` for the first `N` present
|
|
92
|
+
values while **keeping the row count** — mirroring `rolling`'s `minSamples`,
|
|
93
|
+
the one warm-up convention studies use so a smoothed line aligns on its
|
|
94
|
+
source's time axis. (The existing `warmup` option is unchanged — it still
|
|
95
|
+
*drops* the head rows; `minSamples` is the length-preserving counterpart.)
|
|
96
|
+
- **core:** `TimeSeries.rolling` accepts a **count-based** window —
|
|
97
|
+
`rolling({ count: N }, mapping, opts?)` reduces the last / next / centered
|
|
98
|
+
`N` _rows_ (bars) by position instead of a time span. Unlike a duration
|
|
99
|
+
window, an N-bar window stays correct across session gaps (weekends,
|
|
100
|
+
overnight) where `N` bars do not equal `N × barSize` of time — the load-
|
|
101
|
+
bearing primitive for financial N-bar studies (SMA-20, Bollinger-20, rolling
|
|
102
|
+
stdev). Honours `alignment` and `minSamples` exactly like a duration window
|
|
103
|
+
(`minSamples: N` gives the conventional first-`N-1`-rows-`undefined` warmup);
|
|
104
|
+
per-row output only (not supported with a sequence). Same amortized-O(1)-per-
|
|
105
|
+
row sweep as the duration path.
|
|
106
|
+
- **charts:** `<LineChart sessionBreaks>` breaks the line at each trading-axis
|
|
107
|
+
discontinuity (a session / day / lunch close→open) instead of connecting the
|
|
108
|
+
prior close straight to the next open across the collapsed gap. For intraday
|
|
109
|
+
data on a `discontinuities` / `calendar` axis this gives the terminal look —
|
|
110
|
+
the line ends at the close and re-starts at the open, so one session's price
|
|
111
|
+
doesn't visually flow into the next. It's a **scale** break (driven by the
|
|
112
|
+
axis's collapsed gaps), orthogonal to `gaps` (a **data** break, a NaN run) —
|
|
113
|
+
set both independently. Default `false`; a no-op on a continuous axis or a
|
|
114
|
+
provider without `boundaries`. New `Charts/TradingTimeAxis/SessionBreaks`
|
|
115
|
+
story (connected vs broken) + `sessionRuns` / `gappingTicks` helpers.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- **charts:** a **plain continuous time axis** now runs the same ladder as a
|
|
120
|
+
trading-calendar axis (via an internal gap-free identity provider) instead of
|
|
121
|
+
d3's multi-scale default — so a year of daily data ticks cleanly on month
|
|
122
|
+
starts (`Jul Aug … Jun` + the year underneath) rather than mixed
|
|
123
|
+
`"Jun 23" / "Sep" / "Dec"` labels, and its tick count is now width-derived
|
|
124
|
+
like the trading axis's. Month/quarter-grain anchor labels are now bare
|
|
125
|
+
months (`Feb`, was `Feb 02`) with the year on the boundary row;
|
|
126
|
+
`coarsenCalendar`'s finest granularity is renamed `'session'` → `'day'`.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **core:** `TimeSeries.withColumn` now appends an **optional** column
|
|
131
|
+
(`required: false`) rather than a required one. It already accepted
|
|
132
|
+
`(number | undefined)[]`, but marking the column required meant a later
|
|
133
|
+
strict-intake rebuild (`smooth`, any row reconstruction) threw on the gaps —
|
|
134
|
+
so a column with a warm-up (a rolling study fed into an EMA, e.g.
|
|
135
|
+
`ema(sma(bars, …), …)`) crashed. The appended column's type is now
|
|
136
|
+
`number | undefined`, matching `smooth`'s optional append. (`OptionalNumberColumn`
|
|
137
|
+
is exported for the widened return type.)
|
|
138
|
+
|
|
46
139
|
## [0.44.1] — 2026-07-13
|
|
47
140
|
|
|
48
141
|
A `@pond-ts/charts` patch: fixes trading-time axis tick density. `pond-ts`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, AppendColumn, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
|
|
1
|
+
import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, AppendColumn, OptionalNumberColumn, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
|
|
2
2
|
import type { RenameSchema, RollingAlignment, RollingSchema, ColumnValue, DedupeKeep, DiffSchema, FillMapping, FillStrategy, ScalarKind, ScalarValue, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesJsonInput, TimeSeriesInput, TimeRangeKeyedSchema, ValueColumnKindForName, ValueColumnNameForSchema, ValueColumnsForSchema, ValueKeyedSchema } from '../schema/index.js';
|
|
3
3
|
import { type ScanStep } from './operators/scan.js';
|
|
4
4
|
import { ValueSeries } from './value-series.js';
|
|
@@ -1076,10 +1076,22 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1076
1076
|
* Computes sequence-driven rolling aggregations and returns an interval-keyed series on the
|
|
1077
1077
|
* supplied grid.
|
|
1078
1078
|
*
|
|
1079
|
+
* Example: `series.rolling({ count: 20 }, { close: "avg" }, { minSamples: 20 })`.
|
|
1080
|
+
* A **count-based** window of the last `N` *rows* (bars), keyed on row
|
|
1081
|
+
* position not time. `{ count: N }` in place of a duration reduces, for each
|
|
1082
|
+
* row, the `N` rows in its index window — so an "N-bar" study (SMA-20,
|
|
1083
|
+
* Bollinger-20, rolling stdev) stays correct across session gaps (weekends,
|
|
1084
|
+
* overnight) where a duration window silently spans the wrong number of bars.
|
|
1085
|
+
* Honours `alignment` and `minSamples` exactly like a duration window
|
|
1086
|
+
* (`minSamples: N` gives the conventional warmup — the first `N-1` rows emit
|
|
1087
|
+
* `undefined`). Per-row output only; not supported with a sequence.
|
|
1088
|
+
*
|
|
1079
1089
|
* Rolling windows are anchored either at each event's `begin()` time or at the sample point of
|
|
1080
|
-
* each sequence bucket. Membership is determined from source event `begin()` times
|
|
1090
|
+
* each sequence bucket. Membership is determined from source event `begin()` times (a duration
|
|
1091
|
+
* window) or from row position (a `{ count }` window).
|
|
1081
1092
|
*
|
|
1082
|
-
* Supported alignments:
|
|
1093
|
+
* Supported alignments (a `{ count: N }` window applies the same shape in row-index space, with
|
|
1094
|
+
* a centered window biased one row toward `leading` when `N` is even):
|
|
1083
1095
|
* - `"trailing"`: `(t - window, t]`
|
|
1084
1096
|
* - `"leading"`: `[t, t + window)`
|
|
1085
1097
|
* - `"centered"`: `[t - window/2, t + window/2)`
|
|
@@ -1114,7 +1126,9 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1114
1126
|
* therefore read as distinct values here. Scalar columns
|
|
1115
1127
|
* (number / string / boolean) are unaffected (value semantics).
|
|
1116
1128
|
*/
|
|
1117
|
-
rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput
|
|
1129
|
+
rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput | {
|
|
1130
|
+
count: number;
|
|
1131
|
+
}, mapping: Mapping, options?: {
|
|
1118
1132
|
alignment?: RollingAlignment;
|
|
1119
1133
|
minSamples?: number;
|
|
1120
1134
|
}): TimeSeries<RollingSchema<S, Mapping>>;
|
|
@@ -1129,6 +1143,16 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1129
1143
|
* Applies a smoothing transform to one numeric payload column while preserving the original key
|
|
1130
1144
|
* type, key values, and all non-target payload fields.
|
|
1131
1145
|
*
|
|
1146
|
+
* **EMA rate + warm-up.** Give the rate as either `alpha` (0,1] **or** the
|
|
1147
|
+
* financial `span` convention (`α = 2/(span+1)` — a `span`-period EMA, e.g.
|
|
1148
|
+
* `{ span: 12 }`), exactly one of the two. Two warm-up options, both optional:
|
|
1149
|
+
* `warmup: N` **drops** the first N rows (length-changing — hides the noisy
|
|
1150
|
+
* initial convergence); `minSamples: N` **keeps** the rows but emits
|
|
1151
|
+
* `undefined` until N present values have been consumed (length-preserving —
|
|
1152
|
+
* mirrors {@link TimeSeries.rolling}'s `minSamples`, the convention studies
|
|
1153
|
+
* use so a smoothed line aligns on its source's time axis). The EMA converges
|
|
1154
|
+
* on the real values underneath either way.
|
|
1155
|
+
*
|
|
1132
1156
|
* Example: `series.smooth("value", "movingAverage", { window: "5m", alignment: "centered", output: "valueAvg" })`.
|
|
1133
1157
|
* Computes a moving average over the selected numeric column using anchor points derived from
|
|
1134
1158
|
* event keys. `Time` keys use their timestamp. `TimeRange` and `Interval` keys use the midpoint
|
|
@@ -1162,6 +1186,12 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1162
1186
|
smooth<const Target extends NumericColumnNameForSchema<S>, const Output extends string | undefined = undefined>(column: Target, method: SmoothMethod, options: {
|
|
1163
1187
|
alpha: number;
|
|
1164
1188
|
warmup?: number;
|
|
1189
|
+
minSamples?: number;
|
|
1190
|
+
output?: Output;
|
|
1191
|
+
} | {
|
|
1192
|
+
span: number;
|
|
1193
|
+
warmup?: number;
|
|
1194
|
+
minSamples?: number;
|
|
1165
1195
|
output?: Output;
|
|
1166
1196
|
} | {
|
|
1167
1197
|
window: DurationInput;
|
|
@@ -1336,7 +1366,7 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1336
1366
|
* (`fromTrustedColumns`, skipping the finite scan) is a deferred sibling for
|
|
1337
1367
|
* when a perf-critical consumer earns it.
|
|
1338
1368
|
*/
|
|
1339
|
-
withColumn<const Name extends string>(name: Name, values: ReadonlyArray<number | undefined> | Float64Array): TimeSeries<
|
|
1369
|
+
withColumn<const Name extends string>(name: Name, values: ReadonlyArray<number | undefined> | Float64Array): TimeSeries<readonly [S[0], ...ValueColumnsForSchema<S>, OptionalNumberColumn<Name>]>;
|
|
1340
1370
|
/** Example: `series.collapse(["in", "out"], "avg", fn)`. Collapses selected payload fields into a single derived field across each event in the series. */
|
|
1341
1371
|
collapse<const Keys extends readonly (keyof EventDataForSchema<S>)[], Name extends string, R extends ScalarValue>(keys: Keys, output: Name, reducer: (values: Pick<EventDataForSchema<S>, Keys[number]>) => R): TimeSeries<CollapseSchema<S, Keys[number] & string, Name, R>>;
|
|
1342
1372
|
collapse<const Keys extends readonly (keyof EventDataForSchema<S>)[], Name extends string, R extends ScalarValue>(keys: Keys, output: Name, reducer: (values: Pick<EventDataForSchema<S>, Keys[number]>) => R, options: {
|
|
@@ -2114,7 +2114,21 @@ export class TimeSeries {
|
|
|
2114
2114
|
kind: spec.kind,
|
|
2115
2115
|
required: false,
|
|
2116
2116
|
}));
|
|
2117
|
-
|
|
2117
|
+
// A `{ count: N }` window reduces the last/next/centered `N` rows by index
|
|
2118
|
+
// rather than a time span — the count-based (N-bar) window. Detected here so
|
|
2119
|
+
// the duration is never parsed for it.
|
|
2120
|
+
const countWindow = typeof window === 'object' && window !== null && 'count' in window
|
|
2121
|
+
? window.count
|
|
2122
|
+
: null;
|
|
2123
|
+
if (countWindow !== null) {
|
|
2124
|
+
if (!Number.isInteger(countWindow) || countWindow < 1) {
|
|
2125
|
+
throw new TypeError('rolling { count } must be a positive integer (row count)');
|
|
2126
|
+
}
|
|
2127
|
+
if (sequence) {
|
|
2128
|
+
throw new TypeError('rolling count windows ({ count }) are not supported with a sequence');
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const windowMs = countWindow === null ? parseDuration(window) : 0;
|
|
2118
2132
|
const alignment = options.alignment ?? 'trailing';
|
|
2119
2133
|
// Default 0 disables the gate — preserves prior behavior where
|
|
2120
2134
|
// empty windows still invoke the reducer (custom reducers may
|
|
@@ -2229,7 +2243,46 @@ export class TimeSeries {
|
|
|
2229
2243
|
}
|
|
2230
2244
|
}
|
|
2231
2245
|
};
|
|
2232
|
-
if (
|
|
2246
|
+
if (countWindow !== null) {
|
|
2247
|
+
// Count-based window: for each row, the `N` rows in its index window (not
|
|
2248
|
+
// a time span). `lo`/`hi` are both monotonic non-decreasing in the row
|
|
2249
|
+
// index for every alignment, so the same reducer add/remove sweep stays
|
|
2250
|
+
// amortized O(1) per row — each row enters and leaves the window once.
|
|
2251
|
+
// Rows are the unit (no equal-timestamp grouping — `N` counts bars), and a
|
|
2252
|
+
// centered even `N` biases one row toward the leading side.
|
|
2253
|
+
const n = countWindow;
|
|
2254
|
+
const leftSpan = Math.floor((n - 1) / 2);
|
|
2255
|
+
const rightSpan = n - 1 - leftSpan;
|
|
2256
|
+
for (let index = 0; index < rowCount; index++) {
|
|
2257
|
+
let lo;
|
|
2258
|
+
let hi;
|
|
2259
|
+
if (alignment === 'trailing') {
|
|
2260
|
+
lo = index - n + 1 < 0 ? 0 : index - n + 1;
|
|
2261
|
+
hi = index;
|
|
2262
|
+
}
|
|
2263
|
+
else if (alignment === 'leading') {
|
|
2264
|
+
lo = index;
|
|
2265
|
+
hi = index + n - 1 < rowCount ? index + n - 1 : rowCount - 1;
|
|
2266
|
+
}
|
|
2267
|
+
else {
|
|
2268
|
+
lo = index - leftSpan < 0 ? 0 : index - leftSpan;
|
|
2269
|
+
hi = index + rightSpan < rowCount ? index + rightSpan : rowCount - 1;
|
|
2270
|
+
}
|
|
2271
|
+
while (windowEnd <= hi) {
|
|
2272
|
+
addEvent(windowEnd);
|
|
2273
|
+
windowEnd += 1;
|
|
2274
|
+
}
|
|
2275
|
+
while (windowStart < lo) {
|
|
2276
|
+
removeEvent(windowStart);
|
|
2277
|
+
windowStart += 1;
|
|
2278
|
+
}
|
|
2279
|
+
const aggregated = snapshotWindow();
|
|
2280
|
+
for (let c = 0; c < outValues.length; c++) {
|
|
2281
|
+
outValues[c][index] = aggregated[c];
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
else if (alignment === 'trailing') {
|
|
2233
2286
|
for (let groupStart = 0; groupStart < rowCount;) {
|
|
2234
2287
|
const anchor = beginTimes[groupStart];
|
|
2235
2288
|
let groupEnd = groupStart + 1;
|
|
@@ -2323,6 +2376,16 @@ export class TimeSeries {
|
|
|
2323
2376
|
* Applies a smoothing transform to one numeric payload column while preserving the original key
|
|
2324
2377
|
* type, key values, and all non-target payload fields.
|
|
2325
2378
|
*
|
|
2379
|
+
* **EMA rate + warm-up.** Give the rate as either `alpha` (0,1] **or** the
|
|
2380
|
+
* financial `span` convention (`α = 2/(span+1)` — a `span`-period EMA, e.g.
|
|
2381
|
+
* `{ span: 12 }`), exactly one of the two. Two warm-up options, both optional:
|
|
2382
|
+
* `warmup: N` **drops** the first N rows (length-changing — hides the noisy
|
|
2383
|
+
* initial convergence); `minSamples: N` **keeps** the rows but emits
|
|
2384
|
+
* `undefined` until N present values have been consumed (length-preserving —
|
|
2385
|
+
* mirrors {@link TimeSeries.rolling}'s `minSamples`, the convention studies
|
|
2386
|
+
* use so a smoothed line aligns on its source's time axis). The EMA converges
|
|
2387
|
+
* on the real values underneath either way.
|
|
2388
|
+
*
|
|
2326
2389
|
* Example: `series.smooth("value", "movingAverage", { window: "5m", alignment: "centered", output: "valueAvg" })`.
|
|
2327
2390
|
* Computes a moving average over the selected numeric column using anchor points derived from
|
|
2328
2391
|
* event keys. `Time` keys use their timestamp. `TimeRange` and `Interval` keys use the midpoint
|
|
@@ -2364,27 +2427,56 @@ export class TimeSeries {
|
|
|
2364
2427
|
return typeof raw === 'number' ? raw : undefined;
|
|
2365
2428
|
});
|
|
2366
2429
|
if (method === 'ema') {
|
|
2367
|
-
|
|
2368
|
-
|
|
2430
|
+
// Rate parameter: `alpha` directly, or the financial `span` convention
|
|
2431
|
+
// (α = 2/(span+1) — a `span`-period EMA), exactly one of the two.
|
|
2432
|
+
const hasAlpha = 'alpha' in options && options.alpha !== undefined;
|
|
2433
|
+
const hasSpan = 'span' in options && options.span !== undefined;
|
|
2434
|
+
if (hasAlpha === hasSpan) {
|
|
2435
|
+
throw new TypeError('ema smoothing requires exactly one of alpha or span');
|
|
2436
|
+
}
|
|
2437
|
+
let alpha;
|
|
2438
|
+
if (hasSpan) {
|
|
2439
|
+
const span = options.span;
|
|
2440
|
+
if (typeof span !== 'number' || !Number.isFinite(span) || span < 1) {
|
|
2441
|
+
throw new TypeError('ema smoothing requires span to be a finite number >= 1');
|
|
2442
|
+
}
|
|
2443
|
+
alpha = 2 / (span + 1);
|
|
2369
2444
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2445
|
+
else {
|
|
2446
|
+
alpha = options.alpha;
|
|
2447
|
+
if (typeof alpha !== 'number' ||
|
|
2448
|
+
!Number.isFinite(alpha) ||
|
|
2449
|
+
alpha <= 0 ||
|
|
2450
|
+
alpha > 1) {
|
|
2451
|
+
throw new TypeError('ema smoothing requires alpha to be a finite number in the range (0, 1]');
|
|
2452
|
+
}
|
|
2376
2453
|
}
|
|
2377
2454
|
// Optional warm-up: drop the first N output rows to hide the
|
|
2378
2455
|
// noisy initial convergence of the EMA. The smoother still
|
|
2379
2456
|
// processes those events so `previous` is correctly warmed up
|
|
2380
|
-
// by the time we keep a row.
|
|
2457
|
+
// by the time we keep a row. **Length-changing** (rows are removed) —
|
|
2458
|
+
// for the length-preserving warmup that studies want (align on a shared
|
|
2459
|
+
// time axis), use `minSamples` instead.
|
|
2381
2460
|
const warmup = 'warmup' in options && options.warmup !== undefined
|
|
2382
2461
|
? options.warmup
|
|
2383
2462
|
: 0;
|
|
2384
2463
|
if (!Number.isInteger(warmup) || warmup < 0 || !Number.isFinite(warmup)) {
|
|
2385
2464
|
throw new TypeError('ema smoothing requires warmup to be a non-negative integer');
|
|
2386
2465
|
}
|
|
2466
|
+
// Length-preserving warm-up: emit `undefined` until `minSamples` present
|
|
2467
|
+
// source values have been consumed, keeping the row (mirrors `rolling`'s
|
|
2468
|
+
// `minSamples`). The EMA still converges on the real values underneath —
|
|
2469
|
+
// only the emitted value is masked — so the kept tail is unchanged.
|
|
2470
|
+
const minSamples = 'minSamples' in options && options.minSamples !== undefined
|
|
2471
|
+
? options.minSamples
|
|
2472
|
+
: 0;
|
|
2473
|
+
if (!Number.isInteger(minSamples) ||
|
|
2474
|
+
minSamples < 0 ||
|
|
2475
|
+
!Number.isFinite(minSamples)) {
|
|
2476
|
+
throw new TypeError('ema smoothing requires minSamples to be a non-negative integer');
|
|
2477
|
+
}
|
|
2387
2478
|
let previous;
|
|
2479
|
+
let seen = 0;
|
|
2388
2480
|
const resultRows = this.events.map((event) => {
|
|
2389
2481
|
const raw = event.get(column);
|
|
2390
2482
|
const smoothed = typeof raw !== 'number'
|
|
@@ -2394,10 +2486,14 @@ export class TimeSeries {
|
|
|
2394
2486
|
: alpha * raw + (1 - alpha) * previous;
|
|
2395
2487
|
if (smoothed !== undefined) {
|
|
2396
2488
|
previous = smoothed;
|
|
2489
|
+
seen += 1;
|
|
2397
2490
|
}
|
|
2491
|
+
// Mask the emitted value during the length-preserving warm-up, but keep
|
|
2492
|
+
// `previous`/`seen` advancing on the real value above.
|
|
2493
|
+
const emitted = seen < minSamples ? undefined : smoothed;
|
|
2398
2494
|
const nextEvent = output === undefined
|
|
2399
|
-
? event.set(column,
|
|
2400
|
-
: event.merge({ [output]:
|
|
2495
|
+
? event.set(column, emitted)
|
|
2496
|
+
: event.merge({ [output]: emitted });
|
|
2401
2497
|
return Object.freeze([
|
|
2402
2498
|
nextEvent.key(),
|
|
2403
2499
|
...resultSchema
|
|
@@ -2435,7 +2531,7 @@ export class TimeSeries {
|
|
|
2435
2531
|
// `missing: 'skip'` — a cell whose own value is missing stays missing
|
|
2436
2532
|
// (don't fit a fabricated value across the hole from its present
|
|
2437
2533
|
// neighbours). Default `'bridge'` is the prior behaviour (fit everywhere).
|
|
2438
|
-
const skipMissing = options.missing === 'skip';
|
|
2534
|
+
const skipMissing = 'missing' in options && options.missing === 'skip';
|
|
2439
2535
|
const resultRows = this.events.map((event, index) => {
|
|
2440
2536
|
const smoothed = skipMissing && sourceValues[index] === undefined
|
|
2441
2537
|
? undefined
|
|
@@ -2967,9 +3063,15 @@ export class TimeSeries {
|
|
|
2967
3063
|
assertColumnValuesMatchKind('number', values, `withColumn '${String(name)}'`);
|
|
2968
3064
|
const column = columnFromValuesByKind('number', values);
|
|
2969
3065
|
const reshaped = withColumnAppended(this.#store.store, name, column);
|
|
3066
|
+
// The appended column is **optional** (`required: false`): `values` may
|
|
3067
|
+
// carry `undefined` (a missing cell), so the column can have gaps. Marking
|
|
3068
|
+
// it required would make a later strict-intake rebuild (`smooth`, a row
|
|
3069
|
+
// reconstruction) reject those gaps — breaking composition on a column with
|
|
3070
|
+
// a warm-up (e.g. a rolling study fed into an EMA). Matches `smooth`'s
|
|
3071
|
+
// `OptionalNumberColumn` append.
|
|
2970
3072
|
const resultSchema = Object.freeze([
|
|
2971
3073
|
...this.schema,
|
|
2972
|
-
{ name, kind: 'number' },
|
|
3074
|
+
{ name, kind: 'number', required: false },
|
|
2973
3075
|
]);
|
|
2974
3076
|
return TimeSeries.#fromTrustedStore(this.name, resultSchema, reshaped);
|
|
2975
3077
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { ArrayColumn } from './columnar/array-column.js';
|
|
|
30
30
|
export { ChunkedArrayColumn, ChunkedBooleanColumn, ChunkedFloat64Column, ChunkedStringColumn, } from './columnar/chunked-column.js';
|
|
31
31
|
export { type IntervalLabelKind, type KeyColumn, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, ValueKeyColumn, } from './columnar/key-column.js';
|
|
32
32
|
export { type ValidityBitmap } from './columnar/validity.js';
|
|
33
|
-
export type { AlignSchema, ArrayColumnNameForSchema, BaselineSchema, AggregateFunction, AggregateReducer, AggregateOutputMap, AggregateOutputSpec, AggregateMap, AggregateSchema, ValidatedAggregateMap, ColumnDef, CollapseData, ArrayAggregateAppendSchema, ArrayAggregateKind, ArrayAggregateReplaceSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, CollapseSchema, DedupeKeep, EventDataForSchema, EventForSchema, EventKeyForKind, FillMapping, FillStrategy, MaterializeSchema, EventKeyForSchema, FirstColKind, FirstColumn, IntervalKeyedSchema, JsonIntervalInput, JsonObjectRowForSchema, JsonRowFormat, JsonRowForSchema, JsonTimeRangeInput, JsonTimestampInput, JsonValueForKind, RollingAlignment, RollingSchema, JoinConflictMode, JoinManySchema, PrefixedJoinManySchema, PrefixedJoinSchema, LiveSource, JoinType, JoinSchema, NormalizedRowForSchema, NormalizedObjectRowForSchema, NormalizedObjectRow, NormalizedValueForKind, ReduceResult, RenameData, RenameMap, RenameSchema, RekeySchema, RowForSchema, ArrayValue, ColumnValue, ScalarKind, ScalarValue, CustomAggregateReducer, DiffSchema, NumericColumnNameForSchema, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectData, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesInput, TimeSeriesJsonInput, TimeRangeKeyedSchema, ValueColumnsForSchema, ValueColumn, ValueForKind, ValueFirstColumn, ValueKeyedSchema, ValueSeriesColumnName, ValueSeriesSchema, ValueSeriesValueColumns, } from './schema/index.js';
|
|
33
|
+
export type { AlignSchema, ArrayColumnNameForSchema, BaselineSchema, AggregateFunction, AggregateReducer, AggregateOutputMap, AggregateOutputSpec, AggregateMap, AggregateSchema, ValidatedAggregateMap, ColumnDef, CollapseData, ArrayAggregateAppendSchema, ArrayAggregateKind, ArrayAggregateReplaceSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, CollapseSchema, DedupeKeep, EventDataForSchema, EventForSchema, EventKeyForKind, FillMapping, FillStrategy, MaterializeSchema, EventKeyForSchema, FirstColKind, FirstColumn, IntervalKeyedSchema, JsonIntervalInput, JsonObjectRowForSchema, JsonRowFormat, JsonRowForSchema, JsonTimeRangeInput, JsonTimestampInput, JsonValueForKind, RollingAlignment, RollingSchema, JoinConflictMode, JoinManySchema, PrefixedJoinManySchema, PrefixedJoinSchema, LiveSource, JoinType, JoinSchema, NormalizedRowForSchema, NormalizedObjectRowForSchema, NormalizedObjectRow, NormalizedValueForKind, ReduceResult, RenameData, RenameMap, RenameSchema, RekeySchema, RowForSchema, AppendColumn, OptionalNumberColumn, ArrayValue, ColumnValue, ScalarKind, ScalarValue, CustomAggregateReducer, DiffSchema, NumericColumnNameForSchema, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectData, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesInput, TimeSeriesJsonInput, TimeRangeKeyedSchema, ValueColumnsForSchema, ValueColumn, ValueForKind, ValueFirstColumn, ValueKeyedSchema, ValueSeriesColumnName, ValueSeriesSchema, ValueSeriesValueColumns, } from './schema/index.js';
|
|
34
34
|
export type { CalendarOptions, CalendarUnit, TimeZoneOptions, } from './core/calendar.js';
|
|
35
35
|
export type { EventKey, IntervalInput, IntervalValue, TemporalLike, TimeRangeInput, TimestampInput, } from './core/temporal.js';
|
|
36
36
|
export type { DurationInput } from './core/duration.js';
|