openalgo-script 0.6.0 → 0.7.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +12 -12
  3. package/dist/core/engine/channels.js +2 -2
  4. package/dist/core/engine/channels.js.map +1 -1
  5. package/dist/core/engine/engine.js +2 -3
  6. package/dist/core/engine/engine.js.map +1 -1
  7. package/dist/core/engine/library/maths.js +2 -2
  8. package/dist/core/engine/library/maths.js.map +1 -1
  9. package/dist/core/engine/machine.js +5 -5
  10. package/dist/core/engine/machine.js.map +1 -1
  11. package/dist/core/engine/memory.js +2 -2
  12. package/dist/core/engine/memory.js.map +1 -1
  13. package/dist/core/engine/registers.js +2 -2
  14. package/dist/core/engine/registers.js.map +1 -1
  15. package/dist/core/engine/values/index.d.ts +1 -1
  16. package/dist/core/engine/values/index.d.ts.map +1 -1
  17. package/dist/core/engine/values/index.js +1 -1
  18. package/dist/core/engine/values/index.js.map +1 -1
  19. package/dist/core/engine/values/value.d.ts +2 -0
  20. package/dist/core/engine/values/value.d.ts.map +1 -1
  21. package/dist/core/engine/values/value.js +4 -0
  22. package/dist/core/engine/values/value.js.map +1 -1
  23. package/dist/core/stdlib/averages/exponential.d.ts.map +1 -1
  24. package/dist/core/stdlib/averages/exponential.js +3 -6
  25. package/dist/core/stdlib/averages/exponential.js.map +1 -1
  26. package/dist/core/stdlib/averages/gaussian.d.ts +15 -0
  27. package/dist/core/stdlib/averages/gaussian.d.ts.map +1 -0
  28. package/dist/core/stdlib/averages/gaussian.js +55 -0
  29. package/dist/core/stdlib/averages/gaussian.js.map +1 -0
  30. package/dist/core/stdlib/averages/shaped.d.ts +2 -6
  31. package/dist/core/stdlib/averages/shaped.d.ts.map +1 -1
  32. package/dist/core/stdlib/averages/shaped.js +8 -22
  33. package/dist/core/stdlib/averages/shaped.js.map +1 -1
  34. package/dist/core/stdlib/maths/arithmetic.d.ts.map +1 -1
  35. package/dist/core/stdlib/maths/arithmetic.js +2 -1
  36. package/dist/core/stdlib/maths/arithmetic.js.map +1 -1
  37. package/dist/core/stdlib/maths/elementary.d.ts +3 -20
  38. package/dist/core/stdlib/maths/elementary.d.ts.map +1 -1
  39. package/dist/core/stdlib/maths/elementary.js +17 -13
  40. package/dist/core/stdlib/maths/elementary.js.map +1 -1
  41. package/dist/core/stdlib/maths/hypot.d.ts +8 -0
  42. package/dist/core/stdlib/maths/hypot.d.ts.map +1 -0
  43. package/dist/core/stdlib/maths/hypot.js +68 -0
  44. package/dist/core/stdlib/maths/hypot.js.map +1 -0
  45. package/dist/core/stdlib/maths/power.d.ts +3 -0
  46. package/dist/core/stdlib/maths/power.d.ts.map +1 -0
  47. package/dist/core/stdlib/maths/power.js +83 -0
  48. package/dist/core/stdlib/maths/power.js.map +1 -0
  49. package/dist/core/stdlib/maths/transcendental.d.ts +15 -0
  50. package/dist/core/stdlib/maths/transcendental.d.ts.map +1 -0
  51. package/dist/core/stdlib/maths/transcendental.js +185 -0
  52. package/dist/core/stdlib/maths/transcendental.js.map +1 -0
  53. package/dist/core/stdlib/maths/trigonometric.d.ts +4 -0
  54. package/dist/core/stdlib/maths/trigonometric.d.ts.map +1 -0
  55. package/dist/core/stdlib/maths/trigonometric.js +196 -0
  56. package/dist/core/stdlib/maths/trigonometric.js.map +1 -0
  57. package/dist/core/stdlib/series/conditions.d.ts +2 -4
  58. package/dist/core/stdlib/series/conditions.d.ts.map +1 -1
  59. package/dist/core/stdlib/series/conditions.js +9 -13
  60. package/dist/core/stdlib/series/conditions.js.map +1 -1
  61. package/dist/core/stdlib/series/statistics.d.ts.map +1 -1
  62. package/dist/core/stdlib/series/statistics.js +2 -0
  63. package/dist/core/stdlib/series/statistics.js.map +1 -1
  64. package/dist/core/stdlib/values/history.d.ts +26 -0
  65. package/dist/core/stdlib/values/history.d.ts.map +1 -0
  66. package/dist/core/stdlib/values/history.js +52 -0
  67. package/dist/core/stdlib/values/history.js.map +1 -0
  68. package/dist/core/stdlib/values/index.d.ts +1 -0
  69. package/dist/core/stdlib/values/index.d.ts.map +1 -1
  70. package/dist/core/stdlib/values/index.js +1 -0
  71. package/dist/core/stdlib/values/index.js.map +1 -1
  72. package/dist/core/stdlib/values/lookback.d.ts +4 -1
  73. package/dist/core/stdlib/values/lookback.d.ts.map +1 -1
  74. package/dist/core/stdlib/values/lookback.js +48 -45
  75. package/dist/core/stdlib/values/lookback.js.map +1 -1
  76. package/dist/core/stdlib/values/region.d.ts +6 -5
  77. package/dist/core/stdlib/values/region.d.ts.map +1 -1
  78. package/dist/core/stdlib/values/region.js +3 -3
  79. package/dist/core/stdlib/values/region.js.map +1 -1
  80. package/dist/core/stdlib/volatility/measures.d.ts.map +1 -1
  81. package/dist/core/stdlib/volatility/measures.js +6 -4
  82. package/dist/core/stdlib/volatility/measures.js.map +1 -1
  83. package/dist/core/version/version.generated.d.ts +1 -1
  84. package/dist/core/version/version.generated.js +1 -1
  85. package/package.json +9 -1
  86. package/src/core/engine/channels.ts +2 -2
  87. package/src/core/engine/engine.ts +3 -3
  88. package/src/core/engine/library/maths.ts +2 -2
  89. package/src/core/engine/machine.ts +5 -5
  90. package/src/core/engine/memory.ts +2 -2
  91. package/src/core/engine/registers.ts +2 -2
  92. package/src/core/engine/values/index.ts +1 -0
  93. package/src/core/engine/values/value.ts +5 -0
  94. package/src/core/stdlib/averages/exponential.ts +3 -4
  95. package/src/core/stdlib/averages/gaussian.ts +52 -0
  96. package/src/core/stdlib/averages/shaped.ts +8 -21
  97. package/src/core/stdlib/maths/arithmetic.ts +2 -1
  98. package/src/core/stdlib/maths/elementary.ts +20 -33
  99. package/src/core/stdlib/maths/hypot.ts +63 -0
  100. package/src/core/stdlib/maths/power.ts +57 -0
  101. package/src/core/stdlib/maths/transcendental.ts +160 -0
  102. package/src/core/stdlib/maths/trigonometric.ts +160 -0
  103. package/src/core/stdlib/series/conditions.ts +9 -12
  104. package/src/core/stdlib/series/statistics.ts +1 -0
  105. package/src/core/stdlib/values/history.ts +70 -0
  106. package/src/core/stdlib/values/index.ts +1 -0
  107. package/src/core/stdlib/values/lookback.ts +51 -47
  108. package/src/core/stdlib/values/region.ts +7 -5
  109. package/src/core/stdlib/volatility/measures.ts +6 -4
  110. package/src/core/version/version.generated.ts +1 -1
@@ -0,0 +1,160 @@
1
+ /** Directed integer enclosures for the real-result recipe in section 20.10.4. */
2
+ import { bitLength, ceil, floor, parts, rounded, type Interval } from './transcendental.js';
3
+ import { integerSqrt } from './hypot.js';
4
+
5
+ export type TrigonometricKind = 'sin' | 'cos' | 'tan' | 'asin' | 'acos' | 'atan' | 'atan2';
6
+ let cachedPi: { precision: number; value: Interval } = { precision: 0, value: [0n, 0n] };
7
+ const negate = (a: Interval): Interval => [-a[1], -a[0]];
8
+ const add = (a: Interval, b: Interval): Interval => [a[0] + b[0], a[1] + b[1]];
9
+ const subtract = (a: Interval, b: Interval): Interval => [a[0] - b[1], a[1] - b[0]];
10
+ const absolute = (n: bigint): bigint => n < 0n ? -n : n;
11
+
12
+ function ratio(x: number): readonly [bigint, bigint] {
13
+ const [mantissa, exponent] = parts(x);
14
+ return exponent >= 0 ? [mantissa << BigInt(exponent), 1n] : [mantissa, 1n << BigInt(-exponent)];
15
+ }
16
+
17
+ function coarsen(a: Interval, shift: number): Interval {
18
+ const divisor = 1n << BigInt(shift);
19
+ return [floor(a[0], divisor), ceil(a[1], divisor)];
20
+ }
21
+
22
+ function quotient(a: Interval, b: Interval, precision: number): Interval | null {
23
+ if (b[0] <= 0n && b[1] >= 0n) return null;
24
+ const scale = 1n << BigInt(precision);
25
+ let low: bigint | undefined, high: bigint | undefined;
26
+ for (const numerator of a) for (const denominator of b) {
27
+ const n = denominator < 0n ? -numerator : numerator, d = absolute(denominator);
28
+ const lower = floor(n * scale, d), upper = ceil(n * scale, d);
29
+ if (low === undefined || lower < low) low = lower;
30
+ if (high === undefined || upper > high) high = upper;
31
+ }
32
+ return [low!, high!];
33
+ }
34
+
35
+ /** Alternating odd powers, with the first omitted term on its signed side. */
36
+ function smallAtan(n: bigint, d: bigint, precision: number): Interval {
37
+ if (n === 0n) return [0n, 0n];
38
+ if (n < 0n) return negate(smallAtan(-n, d, precision));
39
+ const scale = 1n << BigInt(precision), zl = n * scale / d, zh = ceil(n * scale, d);
40
+ const squaredLow = zl * zl / scale, squaredHigh = ceil(zh * zh, scale);
41
+ let powerLow = zl, powerHigh = zh, low = 0n, high = 0n, odd = 1n, positive = true;
42
+ for (;;) {
43
+ const lower = powerLow / odd, upper = ceil(powerHigh, odd);
44
+ if (positive) { low += lower; high += upper; } else { low -= upper; high -= lower; }
45
+ powerLow = powerLow * squaredLow / scale;
46
+ powerHigh = ceil(powerHigh * squaredHigh, scale);
47
+ odd += 2n; positive = !positive;
48
+ if (powerHigh <= 1n) {
49
+ const tail = ceil(powerHigh, odd);
50
+ return positive ? [low, high + tail] : [low - tail, high];
51
+ }
52
+ }
53
+ }
54
+
55
+ function pi(precision: number): Interval {
56
+ if (cachedPi.precision >= precision) return coarsen(cachedPi.value, cachedPi.precision - precision);
57
+ const a = smallAtan(1n, 5n, precision), b = smallAtan(1n, 239n, precision);
58
+ const value: Interval = [16n * a[0] - 4n * b[1], 16n * a[1] - 4n * b[0]];
59
+ cachedPi = { precision, value };
60
+ return value;
61
+ }
62
+
63
+ function positiveAtan(n: bigint, d: bigint, precision: number): Interval {
64
+ if (n === 0n) return [0n, 0n];
65
+ if (d === 0n) return coarsen(pi(precision), 1);
66
+ if (n > d) return subtract(coarsen(pi(precision), 1), positiveAtan(d, n, precision));
67
+ if (2n * n > d) return add(coarsen(pi(precision), 2), smallAtan(n - d, n + d, precision));
68
+ return smallAtan(n, d, precision);
69
+ }
70
+
71
+ function pointSeries(z: bigint, precision: number, cosine: boolean): Interval {
72
+ if (z < 0n) {
73
+ const out = pointSeries(-z, precision, cosine);
74
+ return cosine ? out : negate(out);
75
+ }
76
+ const scale = 1n << BigInt(precision);
77
+ if (z === 0n) return cosine ? [scale, scale] : [0n, 0n];
78
+ const squaredLow = z * z / scale, squaredHigh = ceil(z * z, scale);
79
+ let low = cosine ? scale : z, high = low, termLow = low, termHigh = low;
80
+ let degree = cosine ? 0n : 1n, positive = false;
81
+ for (;;) {
82
+ const divisor = scale * (degree + 1n) * (degree + 2n);
83
+ termLow = termLow * squaredLow / divisor;
84
+ termHigh = ceil(termHigh * squaredHigh, divisor);
85
+ degree += 2n;
86
+ if (termHigh <= 1n) return positive ? [low, high + termHigh] : [low - termHigh, high];
87
+ if (positive) { low += termLow; high += termHigh; } else { low -= termHigh; high -= termLow; }
88
+ positive = !positive;
89
+ }
90
+ }
91
+
92
+ function reducedSinCos(x: number, precision: number): readonly [Interval, Interval] | null {
93
+ const [n, d] = ratio(x);
94
+ const reductionPrecision = precision + Math.max(0, bitLength(n) - bitLength(d)) + 32;
95
+ const scale = 1n << BigInt(reductionPrecision), [pl, ph] = pi(reductionPrecision);
96
+ const ql = (4n * n * scale + d * ph) / (2n * d * ph);
97
+ const qh = (4n * n * scale + d * pl) / (2n * d * pl);
98
+ if (ql !== qh) return null;
99
+ const residual: Interval = [n * scale / d - ceil(ql * ph, 2n), ceil(n * scale, d) - ql * pl / 2n];
100
+ const [lower, upper] = coarsen(residual, reductionPrecision - precision);
101
+ const unit = 1n << BigInt(precision);
102
+ if (lower < -unit || upper > unit) return null;
103
+ let sine: Interval = [pointSeries(lower, precision, false)[0], pointSeries(upper, precision, false)[1]];
104
+ const a = absolute(lower), b = absolute(upper), far = a > b ? a : b;
105
+ const near = lower <= 0n && upper >= 0n ? 0n : a < b ? a : b;
106
+ let cosine: Interval = [pointSeries(far, precision, true)[0], pointSeries(near, precision, true)[1]];
107
+ switch (Number(ql % 4n)) {
108
+ case 1: [sine, cosine] = [cosine, negate(sine)]; break;
109
+ case 2: [sine, cosine] = [negate(sine), negate(cosine)]; break;
110
+ case 3: [sine, cosine] = [negate(cosine), sine]; break;
111
+ }
112
+ return [x < 0 ? negate(sine) : sine, cosine];
113
+ }
114
+
115
+ function inverse(kind: TrigonometricKind, x: number, y: number, precision: number): Interval {
116
+ if (kind === 'atan') {
117
+ const [n, d] = ratio(x), out = positiveAtan(n, d, precision);
118
+ return x < 0 ? negate(out) : out;
119
+ }
120
+ if (kind === 'atan2') {
121
+ if (x === 0) return y > 0 ? coarsen(pi(precision), 1) : negate(coarsen(pi(precision), 1));
122
+ const [yn, yd] = ratio(y), [xn, xd] = ratio(x);
123
+ let out = positiveAtan(yn * xd, xn * yd, precision);
124
+ if (x < 0) out = subtract(pi(precision), out);
125
+ return y < 0 ? negate(out) : out;
126
+ }
127
+ const [n, d] = ratio(x), scale = 1n << BigInt(precision);
128
+ const radicand = (d * d - n * n) * scale * scale, root = integerSqrt(radicand / (d * d));
129
+ const upper = root + (root * root * d * d === radicand ? 0n : 1n);
130
+ if (kind === 'asin') {
131
+ const out: Interval = [positiveAtan(n * scale, d * upper, precision)[0], positiveAtan(n * scale, d * root, precision)[1]];
132
+ return x < 0 ? negate(out) : out;
133
+ }
134
+ const out: Interval = [positiveAtan(root * d, n * scale, precision)[0], positiveAtan(upper * d, n * scale, precision)[1]];
135
+ return x < 0 ? subtract(pi(precision), out) : out;
136
+ }
137
+
138
+ /** The ordinary argument order is preserved, including atan2(y,x). */
139
+ export function trigonometric(kind: TrigonometricKind, first: number, second?: number, start = 160): number | null {
140
+ if (!Number.isFinite(first) || kind === 'atan2' && !Number.isFinite(second)) return null;
141
+ const x = kind === 'atan2' ? second! : first, y = kind === 'atan2' ? first : 0;
142
+ if ((kind === 'asin' || kind === 'acos') && Math.abs(x) > 1) return null;
143
+ if (kind === 'atan2' && y === 0 && x >= 0) return 0;
144
+ if (x === 0 && kind !== 'atan2') {
145
+ if (kind === 'cos') return 1;
146
+ if (kind !== 'acos') return 0;
147
+ }
148
+ if (kind === 'acos' && x === 1) return 0;
149
+ for (let precision = Math.max(64, start);; precision += 80) {
150
+ let interval: Interval | null;
151
+ if (kind === 'sin' || kind === 'cos' || kind === 'tan') {
152
+ const both = reducedSinCos(x, precision);
153
+ interval = both === null ? null : kind === 'sin' ? both[0] : kind === 'cos' ? both[1] : quotient(both[0], both[1], precision);
154
+ } else interval = inverse(kind, x, y, precision);
155
+ if (interval !== null) {
156
+ const left = rounded(interval[0], -precision), right = rounded(interval[1], -precision);
157
+ if (left === right) return left;
158
+ }
159
+ }
160
+ }
@@ -6,7 +6,7 @@
6
6
  * happened on this bar", which is the one answer a reader would act on.
7
7
  */
8
8
  import type { Flag, Flags, Series, StateRecord, Tail, Value } from '../values/index.js';
9
- import { NONE, fold, queue, slot, tailOf } from '../values/index.js';
9
+ import { ContributionHistory, NONE, fold, slot, tailOf } from '../values/index.js';
10
10
 
11
11
  /** `barsSince(cond)`: bars since the condition last held, 0 on the bar itself. */
12
12
  export function barsSinceStep(state: StateRecord, key: string, cond: Flag): Value {
@@ -37,10 +37,8 @@ export interface Occasion {
37
37
  * `valueWhen(cond, src, occurrence)`: `src` as it stood the last time the
38
38
  * condition held, or the one before that.
39
39
  *
40
- * Only the `occurrence + 1` most recent hits are kept, so the memory is fixed
41
- * by the argument rather than by how much history is loaded. A region whose
42
- * queue grew with the dataset would not be copyable in the sense
43
- * `compiled-program.md` 2.11 requires.
40
+ * A later occurrence can name any earlier true event. Immutable history keeps
41
+ * those values, including absence, while checkpoints share the sealed prefix.
44
42
  */
45
43
  export function valueWhenStep(
46
44
  state: StateRecord,
@@ -49,15 +47,14 @@ export function valueWhenStep(
49
47
  occurrence: number,
50
48
  ): Value {
51
49
  if (!Number.isInteger(occurrence) || occurrence < 0) return NONE;
52
- const wanted = occurrence + 1;
53
- const hits = queue(state, key);
50
+ const held = state[key];
51
+ let hits = held instanceof ContributionHistory ? held : new ContributionHistory();
54
52
  if (input.cond === true) {
55
- hits.push(input.src);
56
- if (hits.length > wanted) hits.shift();
53
+ hits = hits.append(input.src);
54
+ state[key] = hits;
57
55
  }
58
- if (hits.length !== wanted) return NONE;
59
- const first = hits[0];
60
- return typeof first === 'number' ? first : NONE;
56
+ if (hits.count <= occurrence) return NONE;
57
+ return hits.view(occurrence + 1).at(occurrence);
61
58
  }
62
59
 
63
60
  /** `valueWhen(cond, src, occurrence)` as a tail. */
@@ -157,6 +157,7 @@ function jointStep(
157
157
  const b = ordered((back) => right.at(back), len);
158
158
  const m = moments(a, b, len);
159
159
  if (want === 'covariance') return result(m.covariance);
160
+ if (!Number.isFinite(m.varianceA) || !Number.isFinite(m.varianceB)) return NONE;
160
161
  const scale = Math.sqrt(m.varianceA) * Math.sqrt(m.varianceB);
161
162
  if (scale === 0) return NONE;
162
163
  return result(m.covariance / scale);
@@ -0,0 +1,70 @@
1
+ /** Immutable contribution chunks, shared by state checkpoints and independent forks. */
2
+ import type { Value } from './value.js';
3
+
4
+ const WIDTH = 32;
5
+
6
+ interface Chunk {
7
+ readonly values: readonly Value[];
8
+ readonly prior: Chunk | null;
9
+ }
10
+
11
+ /** Appending copies at most one short tail, never the contributed prefix. */
12
+ export class ContributionHistory {
13
+ readonly head: Chunk | null;
14
+ readonly tail: readonly Value[];
15
+ readonly count: number;
16
+
17
+ constructor(
18
+ head: Chunk | null = null,
19
+ tail: readonly Value[] = Object.freeze([]),
20
+ count = 0,
21
+ ) {
22
+ this.head = head;
23
+ this.tail = tail;
24
+ this.count = count;
25
+ Object.freeze(this);
26
+ }
27
+
28
+ append(value: Value): ContributionHistory {
29
+ const full = this.tail.length === WIDTH;
30
+ const head = full ? Object.freeze({ values: this.tail, prior: this.head }) : this.head;
31
+ const tail = Object.freeze(full ? [value] : [...this.tail, value]);
32
+ return new ContributionHistory(head, tail, this.count + 1);
33
+ }
34
+
35
+ /** Index chunk references once; each subsequent newest-first read is constant work. */
36
+ view(length: number): ContributionView {
37
+ const size = Math.min(length, this.count);
38
+ const chunks: (readonly Value[])[] = [];
39
+ let remaining = size - this.tail.length;
40
+ let head = this.head;
41
+ while (remaining > 0 && head !== null) {
42
+ chunks.push(head.values);
43
+ head = head.prior;
44
+ remaining -= WIDTH;
45
+ }
46
+ return new ContributionView(this.tail, chunks, size);
47
+ }
48
+ }
49
+
50
+ /** A transient index into the current window. It is not retained in state. */
51
+ export class ContributionView {
52
+ private readonly tail: readonly Value[];
53
+ private readonly chunks: readonly (readonly Value[])[];
54
+ readonly size: number;
55
+
56
+ constructor(
57
+ tail: readonly Value[], chunks: readonly (readonly Value[])[], size: number,
58
+ ) {
59
+ this.tail = tail;
60
+ this.chunks = chunks;
61
+ this.size = size;
62
+ }
63
+
64
+ at(back: number): Value {
65
+ if (back < 0 || back >= this.size || !Number.isInteger(back)) return null;
66
+ if (back < this.tail.length) return this.tail[this.tail.length - 1 - back]!;
67
+ const before = back - this.tail.length;
68
+ return this.chunks[Math.floor(before / WIDTH)]![WIDTH - 1 - before % WIDTH]!;
69
+ }
70
+ }
@@ -3,6 +3,7 @@ export { NONE, at, isLength, isPresent, result } from './value.js';
3
3
 
4
4
  export type { StateField, StateRecord, StateSlot } from './region.js';
5
5
  export { copyState, flag, held, newState, queue, slot } from './region.js';
6
+ export { ContributionHistory, ContributionView } from './history.js';
6
7
 
7
8
  export type { Tail } from './tail.js';
8
9
  export { fold, tailOf } from './tail.js';
@@ -30,8 +30,9 @@
30
30
  * the length the script asked for rather than by how much history is loaded,
31
31
  * and that is the property a live chart actually needs.
32
32
  */
33
- import type { StateField, StateRecord } from './region.js';
34
- import { newState } from './region.js';
33
+ import type { StateRecord } from './region.js';
34
+ import { newState, slot } from './region.js';
35
+ import { ContributionHistory } from './history.js';
35
36
  import type { Value } from './value.js';
36
37
  import { NONE, isLength, isPresent, result } from './value.js';
37
38
 
@@ -39,7 +40,7 @@ import { NONE, isLength, isPresent, result } from './value.js';
39
40
  export interface Lookback {
40
41
  /** Advance by one bar. */
41
42
  push(value: Value): void;
42
- /** True once `len` bars have gone by, present or not: this is warmup. */
43
+ /** True after the largest observed length's contributions, present or not. */
43
44
  filled(): boolean;
44
45
  /** True when the lookback is filled and every bar in it has a value. */
45
46
  complete(): boolean;
@@ -64,48 +65,28 @@ export interface Lookback {
64
65
  */
65
66
  export function ring(record: StateRecord, key: string, len: number | null): Lookback {
66
67
  const size = len !== null && isLength(len) ? len : 0;
67
- const bars = `${key}:q`;
68
- const nextKey = `${key}:n`;
69
- const seenKey = `${key}:s`;
68
+ const bars = `${key}:h`;
69
+ const maximumKey = `${key}:m`;
70
70
  const held = record[bars];
71
- let items: StateField[];
72
- if (Array.isArray(held) && held.length === size) {
73
- items = held;
74
- } else {
75
- items = new Array<StateField>(size).fill(NONE);
76
- record[bars] = items;
77
- record[nextKey] = 0;
78
- record[seenKey] = 0;
79
- }
80
-
81
- const count = (name: string): number => {
82
- const value = record[name];
83
- return typeof value === 'number' ? value : 0;
84
- };
85
- const index = (back: number): number => {
86
- const from = count(nextKey) - 1 - back;
87
- return ((from % size) + size) % size;
88
- };
71
+ let history = held instanceof ContributionHistory ? held : new ContributionHistory();
72
+ const before = record[maximumKey];
73
+ const maximum = Math.max(typeof before === 'number' ? before : 0, size);
74
+ record[maximumKey] = maximum;
75
+ let view = history.view(size);
76
+ return lookback(size, (value) => {
77
+ history = history.append(value);
78
+ record[bars] = history;
79
+ view = history.view(size);
80
+ }, () => size > 0 && history.count >= maximum, (back) => view.at(back));
81
+ }
89
82
 
83
+ /** The arithmetic is shared so fixed-length accumulation keeps its exact order. */
84
+ function lookback(
85
+ size: number, push: (value: Value) => void, filled: () => boolean,
86
+ at: (back: number) => Value,
87
+ ): Lookback {
90
88
  const lookback: Lookback = {
91
- push(value: Value): void {
92
- if (size === 0) return;
93
- items[count(nextKey)] = value;
94
- const next = count(nextKey) + 1;
95
- record[nextKey] = next === size ? 0 : next;
96
- if (count(seenKey) < size) record[seenKey] = count(seenKey) + 1;
97
- },
98
-
99
- filled(): boolean {
100
- return size > 0 && count(seenKey) === size;
101
- },
102
-
103
- at(back: number): Value {
104
- if (size === 0 || back < 0 || back >= size) return NONE;
105
- const value = items[index(back)];
106
- return typeof value === 'number' ? value : NONE;
107
- },
108
-
89
+ push, filled, at,
109
90
  presentCount(): number {
110
91
  let counted = 0;
111
92
  for (let back = size - 1; back >= 0; back -= 1) {
@@ -170,6 +151,9 @@ export type Recurrence = (previous: number, value: number) => number;
170
151
  * next present bar continues from where the last one left off. The alternatives
171
152
  * are worse: consuming absence as zero would drag the average toward nothing,
172
153
  * and re-seeding would let one missing bar restart a two hundred bar average.
154
+ * Missing lengths also freeze it. Readiness tracks all contributions and the
155
+ * largest observed length, independently from the running value. A valid step
156
+ * still advances that value while a later, larger length hides its output.
173
157
  */
174
158
  export function smoothed(
175
159
  record: StateRecord,
@@ -178,21 +162,41 @@ export function smoothed(
178
162
  value: Value,
179
163
  step: Recurrence,
180
164
  ): Value {
181
- const seed = ring(record, `${key}~`, len);
165
+ const countKey = `${key}:n`;
166
+ const maximumKey = `${key}:m`;
167
+ const historyKey = `${key}~:h`;
182
168
  const runningKey = `${key}:r`;
183
169
  const startedKey = `${key}:o`;
170
+ const valid = len !== null && isLength(len);
171
+ const count = slot(record, countKey, 0) + 1;
172
+ const maximum = Math.max(slot(record, maximumKey, 0), valid ? len : 0);
173
+ record[countKey] = count;
174
+ record[maximumKey] = maximum;
184
175
  if (record[startedKey] !== true) {
185
- seed.push(value);
186
- const mean = seed.mean();
176
+ const held = record[historyKey];
177
+ const before = held instanceof ContributionHistory ? held : new ContributionHistory();
178
+ const history = before.append(value);
179
+ record[historyKey] = history;
180
+ if (!valid || count < maximum) return NONE;
181
+ const view = history.view(len);
182
+ let total = 0;
183
+ for (let back = len - 1; back >= 0; back -= 1) {
184
+ const next = view.at(back);
185
+ if (!isPresent(next)) return NONE;
186
+ total += next;
187
+ }
188
+ const mean = result(total / len);
187
189
  if (!isPresent(mean)) return NONE;
188
190
  record[startedKey] = true;
189
191
  record[runningKey] = mean;
192
+ // Checkpoints retain their immutable version; this live state needs only scalars.
193
+ delete record[historyKey];
190
194
  return result(mean);
191
195
  }
192
- if (!isPresent(value)) return NONE;
196
+ if (!valid || !isPresent(value)) return NONE;
193
197
  const previous = record[runningKey];
194
198
  if (typeof previous !== 'number') return NONE;
195
199
  const next = step(previous, value);
196
200
  record[runningKey] = next;
197
- return result(next);
201
+ return count >= maximum ? result(next) : NONE;
198
202
  }
@@ -11,8 +11,8 @@
11
11
  * is what 6.2 refuses in as many words: an engine does not re-seed an average
12
12
  * from history, it puts back the record it copied.
13
13
  *
14
- * So a region is a flat record of numbers, booleans, strings and fixed length
15
- * queues, and `copyState` is the mechanical copy. Every function in this
14
+ * So a region is a flat record of scalars, bounded mutable queues and immutable
15
+ * contribution history, and `copyState` is the mechanical copy. Every function in this
16
16
  * library is written against one, and the tail form in `tail.ts` is the same
17
17
  * step over a region nobody else can reach. That is what lets the one
18
18
  * implementation serve both a library call folded over a series and an engine
@@ -23,11 +23,13 @@
23
23
  * of them.
24
24
  */
25
25
 
26
- /** One field of a state region. A queue is the one compound form 2.11 allows. */
26
+ import type { ContributionHistory } from './history.js';
27
+
28
+ /** One scalar field of a state region. */
27
29
  export type StateField = number | boolean | string | null;
28
30
 
29
31
  /** What a single key holds. */
30
- export type StateSlot = StateField | StateField[];
32
+ export type StateSlot = StateField | StateField[] | ContributionHistory;
31
33
 
32
34
  /** A state region: a flat record, copyable without knowing what owns it. */
33
35
  export type StateRecord = Record<string, StateSlot | undefined>;
@@ -37,7 +39,7 @@ export function newState(): StateRecord {
37
39
  return {};
38
40
  }
39
41
 
40
- /** The mechanical copy 2.11 asks for: one level, with queues cloned. */
42
+ /** Mutable queues are cloned; immutable history versions are safely shared. */
41
43
  export function copyState(record: StateRecord): StateRecord {
42
44
  const out: StateRecord = {};
43
45
  for (const key of Object.keys(record)) {
@@ -9,6 +9,7 @@
9
9
  import type { Bar, Series, StateRecord, Tail, Value } from '../values/index.js';
10
10
  import { NONE, fold, isPresent, result, tailOf } from '../values/index.js';
11
11
  import { extremeStep, historyStep, sumStep } from '../series/index.js';
12
+ import { log, log10 } from '../maths/index.js';
12
13
 
13
14
  import type { Gap } from './range.js';
14
15
  import { gapOf, trueRangeOf } from './range.js';
@@ -41,9 +42,10 @@ export function chopStep(
41
42
  if (len === null) return NONE;
42
43
  if (!isPresent(distance) || !isPresent(upper) || !isPresent(lower)) return NONE;
43
44
  const span = upper - lower;
44
- const scale = Math.log10(len);
45
- if (!(span > 0) || !(distance > 0) || scale === 0) return NONE;
46
- return result((100 * Math.log10(distance / span)) / scale);
45
+ const scale = log10(len);
46
+ if (!(span > 0) || !(distance > 0) || !isPresent(scale) || scale === 0) return NONE;
47
+ const travelled = log10(distance / span);
48
+ return isPresent(travelled) ? result((100 * travelled) / scale) : NONE;
47
49
  }
48
50
 
49
51
  /** `chop(len)` as a tail. */
@@ -78,7 +80,7 @@ export function hvStep(
78
80
  const previous = historyStep(state, `${key}h`, value, 1);
79
81
  let logReturn: Value = NONE;
80
82
  if (isPresent(value) && isPresent(previous) && value > 0 && previous > 0) {
81
- logReturn = result(Math.log(value / previous));
83
+ logReturn = log(value / previous);
82
84
  }
83
85
  const deviation = stdevStep(state, `${key}q`, logReturn, len, false);
84
86
  if (!isPresent(deviation) || periodsPerYear === null || !(periodsPerYear > 0)) return NONE;
@@ -4,7 +4,7 @@
4
4
  // spec/compiled-program.md, so neither can drift from the thing it names.
5
5
 
6
6
  /** This build's package version. The release refuses to publish unless the tag, the manifest and this agree. */
7
- export const VERSION = "0.6.0";
7
+ export const VERSION = "0.7.0";
8
8
 
9
9
  /**
10
10
  * The compiled program format this compiler emits.