oakscriptjs 0.11.1 → 0.11.2

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 (60) hide show
  1. package/dist/cjs/callsite/index.d.ts.map +1 -1
  2. package/dist/cjs/callsite/index.js +5 -2
  3. package/dist/cjs/callsite/index.js.map +2 -2
  4. package/dist/cjs/index.d.ts +2 -2
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +2 -1
  7. package/dist/cjs/index.js.map +2 -2
  8. package/dist/cjs/script/index.d.ts +1 -1
  9. package/dist/cjs/script/index.d.ts.map +1 -1
  10. package/dist/cjs/script/index.js +6 -51
  11. package/dist/cjs/script/index.js.map +2 -2
  12. package/dist/cjs/session/calendar.d.ts.map +1 -1
  13. package/dist/cjs/session/calendar.js +1 -0
  14. package/dist/cjs/session/calendar.js.map +2 -2
  15. package/dist/cjs/session/context.d.ts +20 -4
  16. package/dist/cjs/session/context.d.ts.map +1 -1
  17. package/dist/cjs/session/context.js +65 -5
  18. package/dist/cjs/session/context.js.map +2 -2
  19. package/dist/cjs/ta/index.d.ts +31 -29
  20. package/dist/cjs/ta/index.d.ts.map +1 -1
  21. package/dist/cjs/ta/index.js +10 -12
  22. package/dist/cjs/ta/index.js.map +3 -3
  23. package/dist/cjs/table/index.d.ts +10 -5
  24. package/dist/cjs/table/index.d.ts.map +1 -1
  25. package/dist/cjs/table/index.js +11 -2
  26. package/dist/cjs/table/index.js.map +2 -2
  27. package/dist/esm/callsite/index.d.ts.map +1 -1
  28. package/dist/esm/callsite/index.js +5 -2
  29. package/dist/esm/callsite/index.js.map +1 -1
  30. package/dist/esm/index.d.ts +2 -2
  31. package/dist/esm/index.d.ts.map +1 -1
  32. package/dist/esm/index.js +5 -4
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/script/index.d.ts +1 -1
  35. package/dist/esm/script/index.d.ts.map +1 -1
  36. package/dist/esm/script/index.js +7 -62
  37. package/dist/esm/script/index.js.map +1 -1
  38. package/dist/esm/session/calendar.d.ts.map +1 -1
  39. package/dist/esm/session/calendar.js +4 -0
  40. package/dist/esm/session/calendar.js.map +1 -1
  41. package/dist/esm/session/context.d.ts +20 -4
  42. package/dist/esm/session/context.d.ts.map +1 -1
  43. package/dist/esm/session/context.js +81 -7
  44. package/dist/esm/session/context.js.map +1 -1
  45. package/dist/esm/ta/index.d.ts +31 -29
  46. package/dist/esm/ta/index.d.ts.map +1 -1
  47. package/dist/esm/ta/index.js +45 -45
  48. package/dist/esm/ta/index.js.map +1 -1
  49. package/dist/esm/table/index.d.ts +10 -5
  50. package/dist/esm/table/index.d.ts.map +1 -1
  51. package/dist/esm/table/index.js +23 -8
  52. package/dist/esm/table/index.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/callsite/index.ts +5 -2
  55. package/src/index.ts +6 -3
  56. package/src/script/index.ts +7 -65
  57. package/src/session/calendar.ts +3 -0
  58. package/src/session/context.ts +99 -7
  59. package/src/ta/index.ts +46 -45
  60. package/src/table/index.ts +22 -6
@@ -122,7 +122,7 @@ import {
122
122
  type StrategyTrade,
123
123
  type StrategyVariable,
124
124
  } from '../strategy/index.js';
125
- import { parsePineSession, sessionRun } from '../time/session.js';
125
+ import { sessionPeriods, type SessionPeriods } from '../session/context.js';
126
126
 
127
127
  /** Everything one run of a script produced. */
128
128
  export interface ScriptRunResult {
@@ -1894,37 +1894,6 @@ function sessionBars(): { bars: SessionBars; regular: SessionBars } {
1894
1894
  return sessionCache;
1895
1895
  }
1896
1896
 
1897
- /**
1898
- * Calendar for `time(tf, session, timezone)` arguments. Without session and time zone: the loaded
1899
- * session. As in PineScript, an explicit time zone reads the symbol session hours in that zone without
1900
- * corrections and holidays, and an explicit session follows PineScript's session string rules.
1901
- */
1902
- function calendarFor(
1903
- session?: string,
1904
- timezone?: string,
1905
- dayPlacement = false
1906
- ): { bars: SessionBars; test: boolean; inside?: (time: number) => boolean } {
1907
- const { bars } = sessionBars();
1908
- if (!session && !timezone) return { bars, test: false };
1909
- const tz = timezone ?? exchangeTimezone();
1910
- // a session string that is empty or does not start with a digit is the symbol session: "regular" the regular
1911
- // hours, "extended" and any other name the loaded hours (PineScript)
1912
- if (session !== undefined && parsePineSession(session) === null) {
1913
- const name = session.trim();
1914
- const symbol = name === 'regular' && chartCtx.regularSession ? chartCtx.regularSession : chartCtx.session!;
1915
- return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
1916
- }
1917
- const spec = session ? { session } : { session: toSpec(chartCtx.session!).session };
1918
- return {
1919
- bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol', dayPlacement), bars.chart.period),
1920
- test: true,
1921
- // a PineScript session string: the rules of time.inSession (runs ending at 0000, days around a clock change)
1922
- inside: session
1923
- ? (time) => timeCore.inSession(time, session, tz, timeframeCore.in_seconds(bars.chart.period) * 1000)
1924
- : undefined,
1925
- };
1926
- }
1927
-
1928
1897
  function perBar(fn: (t: number, i: number, times: number[]) => number): Series {
1929
1898
  const times = ctxBars.bars.map(barTime);
1930
1899
  return Series.fromArray(ctxBars, times.map((t, i) => fn(t, i, times)));
@@ -1937,48 +1906,21 @@ function callableSeries<F extends (...args: never[]) => Series>(series: Series,
1937
1906
  return fn as Series & F;
1938
1907
  }
1939
1908
 
1940
- /**
1941
- * `time(tf, session)` / `time_close(tf, session)` of a bar, as PineScript computes them (#173): an intraday timeframe
1942
- * tests the bar with the session rules of time.inSession and aligns its periods on the start of the run that
1943
- * contains the bar; "D" tests the bar against the day's session period (the calendar); a timeframe of more than one
1944
- * day ("2D", "W", "M") has no test and keeps the period's time.
1945
- */
1946
- function sessionPeriods(timeframeString: string, session?: string, timezone?: string): { open: (t: number) => number; close: (t: number) => number } {
1947
- const tf = timeframeCore.info(timeframeString);
1948
- const { bars, test, inside } = calendarFor(session, timezone, !tf.isintraday);
1949
- const open = (t: number): number => bars.timeOf(t, timeframeString);
1950
- const close = (t: number): number => bars.closeOf(t, timeframeString);
1951
- if (!test) return { open, close };
1952
- if (tf.isintraday) {
1953
- if (session !== undefined && parsePineSession(session) !== null && !bars.chart.isdwm) {
1954
- const tz = timezone ?? exchangeTimezone();
1955
- const barMs = timeframeCore.in_seconds(bars.chart.period) * 1000;
1956
- const ms = timeframeCore.in_seconds(tf.period) * 1000;
1957
- const run = (t: number) => sessionRun(t, session, tz, barMs);
1958
- const start = (t: number, r: [number, number]) => r[0] + Math.floor((t - r[0]) / ms) * ms;
1959
- return {
1960
- open: (t) => { const r = run(t); return r ? start(t, r) : NaN; },
1961
- close: (t) => { const r = run(t); return r ? Math.min(start(t, r) + ms, r[1]) : NaN; },
1962
- };
1963
- }
1964
- const isIn = (t: number) => (inside ? inside(t) : bars.inside(t));
1965
- return { open: (t) => (isIn(t) ? open(t) : NaN), close: (t) => (isIn(t) ? close(t) : NaN) };
1966
- }
1967
- if (tf.isdaily && tf.multiplier === 1) {
1968
- return { open: (t) => (bars.inside(t) ? open(t) : NaN), close: (t) => (bars.inside(t) ? close(t) : NaN) };
1969
- }
1970
- return { open, close };
1909
+ /** `time(tf, session, timezone)` / `time_close(...)` periods of the run's chart (see session/context `sessionPeriods`). */
1910
+ function chartSessionPeriods(timeframeString: string, session?: string, timezone?: string): SessionPeriods {
1911
+ const { bars } = sessionBars();
1912
+ return sessionPeriods(bars, { ...chartCtx, timezone: exchangeTimezone() }, timeframeString, session, timezone);
1971
1913
  }
1972
1914
 
1973
1915
  function timeFunction(tf?: string, session?: string, timezone?: string): Series {
1974
1916
  collector();
1975
- const { open } = sessionPeriods(tf || chartTimeframe().period, session, timezone);
1917
+ const { open } = chartSessionPeriods(tf || chartTimeframe().period, session, timezone);
1976
1918
  return perBar((t) => open(t));
1977
1919
  }
1978
1920
 
1979
1921
  function timeCloseFunction(tf?: string, session?: string, timezone?: string): Series {
1980
1922
  collector();
1981
- const { close } = sessionPeriods(tf || chartTimeframe().period, session, timezone);
1923
+ const { close } = chartSessionPeriods(tf || chartTimeframe().period, session, timezone);
1982
1924
  return perBar((t) => close(t));
1983
1925
  }
1984
1926
 
@@ -197,6 +197,9 @@ export class TradingCalendar {
197
197
  ? [at(d - p.startOffset, p.start, true), at(d - p.endOffset, p.end - 1, true) + 60_000]
198
198
  : [at(d - p.startOffset, p.start), at(d - p.endOffset, p.end)]
199
199
  );
200
+ // for "D", the runs of a PineScript session string on one day are one period, from the earliest start to the
201
+ // latest end: the time between two runs is inside the day (#182)
202
+ if (this.pine && out.length > 1) out = [[Math.min(...out.map((p) => p[0])), Math.max(...out.map((p) => p[1]))]];
200
203
  this.cache.set(date, out);
201
204
  return out;
202
205
  }
@@ -11,6 +11,7 @@
11
11
  import type { Bar } from '../types/index.js';
12
12
  import { info, in_seconds } from '../timeframe/index.js';
13
13
  import { afterCompletion, periodsOf, resample } from '../security/resample.js';
14
+ import { parsePineSession, sessionRun } from '../time/session.js';
14
15
  import { SessionBars, sessionFlags, strayPeriods, type SessionFlags } from './bars.js';
15
16
  import { TradingCalendar, type SessionSpec } from './calendar.js';
16
17
 
@@ -61,16 +62,107 @@ export function chartSessions(chart: SessionContext): ChartSessions {
61
62
  return out;
62
63
  }
63
64
 
64
- /** PineScript `time(timeframe)` (ms) of a bar that opens at `time`: the start of its period; NaN without one. */
65
- export function periodTime(time: number, timeframe: string, chart: SessionContext): number {
66
- const s = chartSessions(chart);
67
- return s.bars.timeOf(time * s.unit, timeframe);
65
+ /** Open and close (ms) of the period that contains a bar opening at a time in ms; NaN outside the session. */
66
+ export interface SessionPeriods {
67
+ open: (time: number) => number;
68
+ close: (time: number) => number;
69
+ }
70
+
71
+ /**
72
+ * Calendar for the `session` / `timezone` arguments of `time(tf, session, timezone)`. Without both: the loaded
73
+ * session. As in PineScript, an explicit time zone reads the symbol session hours in that zone without corrections
74
+ * and holidays, and an explicit session follows PineScript's session string rules.
75
+ */
76
+ function calendarFor(
77
+ bars: SessionBars,
78
+ chart: SessionContext,
79
+ session: string | undefined,
80
+ timezone: string | undefined,
81
+ dayPlacement: boolean
82
+ ): { bars: SessionBars; test: boolean; inside?: (time: number) => boolean } {
83
+ if (!session && !timezone) return { bars, test: false };
84
+ const tz = timezone ?? chart.timezone!;
85
+ // a session string that is empty or does not start with a digit is the symbol session: "regular" the regular
86
+ // hours, "extended" and any other name the loaded hours (PineScript)
87
+ if (session !== undefined && parsePineSession(session) === null) {
88
+ const name = session.trim();
89
+ const symbol = name === 'regular' && chart.regularSession ? chart.regularSession : chart.session!;
90
+ return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
91
+ }
92
+ const spec = session ? { session } : { session: toSpec(chart.session!).session };
93
+ return {
94
+ bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol', dayPlacement), bars.chart.period),
95
+ test: true,
96
+ // a PineScript session string: the rules of time.inSession (runs ending at 0000, days around a clock change)
97
+ inside: session ? (time) => sessionRun(time, session, tz, in_seconds(bars.chart.period) * 1000) !== null : undefined,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * `time(tf, session, timezone)` / `time_close(tf, session, timezone)` of a bar, as PineScript computes them (#173):
103
+ * an intraday timeframe tests the bar with the session rules of time.inSession and aligns its periods on the start
104
+ * of the run that contains the bar; "D" tests the bar against the day's session period (the calendar; the time
105
+ * between two runs of a day is inside it, #182); a timeframe of more than one day ("2D", "W", "M") has no test and
106
+ * keeps the period's time. `bars` is the calendar of the loaded session (`chartSessions(chart).bars`).
107
+ */
108
+ export function sessionPeriods(
109
+ bars: SessionBars,
110
+ chart: SessionContext,
111
+ timeframe: string,
112
+ session?: string,
113
+ timezone?: string
114
+ ): SessionPeriods {
115
+ const tf = info(timeframe);
116
+ const { bars: calendar, test, inside } = calendarFor(bars, chart, session, timezone, !tf.isintraday);
117
+ const open = (t: number): number => calendar.timeOf(t, timeframe);
118
+ const close = (t: number): number => calendar.closeOf(t, timeframe);
119
+ if (!test) return { open, close };
120
+ if (tf.isintraday) {
121
+ if (session !== undefined && parsePineSession(session) !== null && !calendar.chart.isdwm) {
122
+ const tz = timezone ?? chart.timezone!;
123
+ const barMs = in_seconds(calendar.chart.period) * 1000;
124
+ const ms = in_seconds(tf.period) * 1000;
125
+ const run = (t: number) => sessionRun(t, session, tz, barMs);
126
+ const start = (t: number, r: [number, number]) => r[0] + Math.floor((t - r[0]) / ms) * ms;
127
+ return {
128
+ open: (t) => { const r = run(t); return r ? start(t, r) : NaN; },
129
+ close: (t) => { const r = run(t); return r ? Math.min(start(t, r) + ms, r[1]) : NaN; },
130
+ };
131
+ }
132
+ const isIn = (t: number) => (inside ? inside(t) : calendar.inside(t));
133
+ return { open: (t) => (isIn(t) ? open(t) : NaN), close: (t) => (isIn(t) ? close(t) : NaN) };
134
+ }
135
+ if (tf.isdaily && tf.multiplier === 1) {
136
+ return { open: (t) => (calendar.inside(t) ? open(t) : NaN), close: (t) => (calendar.inside(t) ? close(t) : NaN) };
137
+ }
138
+ return { open, close };
68
139
  }
69
140
 
70
- /** PineScript `time_close(timeframe)` (ms) of a bar that opens at `time`. */
71
- export function periodClose(time: number, timeframe: string, chart: SessionContext): number {
141
+ const periodCache = new WeakMap<object, Map<string, SessionPeriods>>();
142
+
143
+ function periodsOfChart(chart: SessionContext, timeframe: string, session?: string, timezone?: string): { s: ChartSessions; p: SessionPeriods } {
72
144
  const s = chartSessions(chart);
73
- return s.bars.closeOf(time * s.unit, timeframe);
145
+ let byKey = periodCache.get(chart);
146
+ if (!byKey) periodCache.set(chart, (byKey = new Map()));
147
+ const key = `${timeframe}|${session ?? ''}|${timezone ?? ''}`;
148
+ let p = byKey.get(key);
149
+ if (!p) byKey.set(key, (p = sessionPeriods(s.bars, chart, timeframe, session, timezone)));
150
+ return { s, p };
151
+ }
152
+
153
+ /**
154
+ * PineScript `time(timeframe, session, timezone)` (ms) of a bar that opens at `time`: the start of its period; NaN
155
+ * without one or outside `session` (#182). Without `session` and `timezone`, the periods of the loaded session.
156
+ */
157
+ export function periodTime(time: number, timeframe: string, chart: SessionContext, session?: string, timezone?: string): number {
158
+ const { s, p } = periodsOfChart(chart, timeframe, session, timezone);
159
+ return p.open(time * s.unit);
160
+ }
161
+
162
+ /** PineScript `time_close(timeframe, session, timezone)` (ms) of a bar that opens at `time` (#182). */
163
+ export function periodClose(time: number, timeframe: string, chart: SessionContext, session?: string, timezone?: string): number {
164
+ const { s, p } = periodsOfChart(chart, timeframe, session, timezone);
165
+ return p.close(time * s.unit);
74
166
  }
75
167
 
76
168
  /** PineScript `time_tradingday` (ms, 00:00 UTC of the trading day) of a bar that opens at `time`. */
package/src/ta/index.ts CHANGED
@@ -138,7 +138,8 @@ export function rsi(source: Source, length: simple_int): series_float;
138
138
  /**
139
139
  * PineScript v4 `rsi(x, y)` with two series (#180): `x` and `y` are the averages of the upward and downward changes,
140
140
  * already computed. As in PineScript: 100 when `y` is within 1e-10 of zero (also with an na `x`), else 0 when `x` is,
141
- * else `100 - 100 / (1 + x / y)`, na when that divides by zero (`x == -y`). Versions 5 and 6 have the length form only.
141
+ * else `100 - 100 / (1 + x / y)`: -Infinity when `x == -y` (#182), which `na()` and `nz()` treat as na while comparisons
142
+ * see it (`r < 30` is true). Versions 5 and 6 have the length form only.
142
143
  */
143
144
  export function rsi(x: Source, y: Source): series_float;
144
145
  export function rsi(source: Source, length: simple_int | Source): series_float {
@@ -148,8 +149,7 @@ export function rsi(source: Source, length: simple_int | Source): series_float {
148
149
  const y = length[i] ?? NaN;
149
150
  if (eq(y, 0)) return 100;
150
151
  if (eq(x, 0)) return 0;
151
- const r = 100 - 100 / (1 + x / y);
152
- return Number.isFinite(r) ? r : NaN;
152
+ return 100 - 100 / (1 + x / y);
153
153
  });
154
154
  }
155
155
  checkPositiveLength('rsi', length, 0, 'y');
@@ -293,14 +293,14 @@ export function stdev(
293
293
  }
294
294
 
295
295
  /**
296
- * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist.
297
- * `isNa` tells which values are skipped (NaN by default).
296
+ * The last `length` non-na values up to bar `i` (the bar itself may be na), or null when fewer exist. +/-Infinity
297
+ * counts as na (#183).
298
298
  */
299
299
  function lastValues(source: Source, i: number, length: number): number[] | null {
300
300
  const values: number[] = [];
301
301
  for (let j = i; j >= 0 && values.length < length; j--) {
302
302
  const x = source[j]!;
303
- if (!Number.isNaN(x)) values.push(x);
303
+ if (Number.isFinite(x)) values.push(x);
304
304
  }
305
305
  return values.length === length ? values : null;
306
306
  }
@@ -361,7 +361,7 @@ function windowExtremes(
361
361
  /**
362
362
  * The non-na values of `source` with their bars, and for each bar the position of its last non-na value (-1 when
363
363
  * there is none yet). PineScript's ta.highest / ta.lowest / ta.highestbars / ta.lowestbars / ta.pivothigh /
364
- * ta.pivotlow keep the non-na values they receive and nothing else.
364
+ * ta.pivotlow keep the non-na values they receive and nothing else; +/-Infinity is skipped like na (#183).
365
365
  */
366
366
  function keptValues(source: Source): { values: number[]; bars: number[]; last: Int32Array } {
367
367
  const values: number[] = [];
@@ -369,7 +369,7 @@ function keptValues(source: Source): { values: number[]; bars: number[]; last: I
369
369
  const last = new Int32Array(source.length);
370
370
  for (let i = 0; i < source.length; i++) {
371
371
  const x = source[i]!;
372
- if (!Number.isNaN(x)) {
372
+ if (Number.isFinite(x)) {
373
373
  values.push(x);
374
374
  bars.push(i);
375
375
  }
@@ -910,9 +910,9 @@ export function wma(source: Source, length: simple_int | ArrayLike<number>): ser
910
910
  * @returns Highest value series (na on the first `length - 1` bars)
911
911
  *
912
912
  * @remarks
913
- * As in PineScript: `na` values are skipped. The result is the highest of the last `length` non-na values (of all
914
- * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
915
- * value was received. A fractional length is truncated (7.5 is 7 bars).
913
+ * As in PineScript: `na` values (also +/-Infinity, #183) are skipped. The result is the highest of the last `length`
914
+ * non-na values (of all of them when there are fewer), also on a bar whose own value is `na`; `na` before bar
915
+ * `length - 1` and while no value was received. A fractional length is truncated (7.5 is 7 bars).
916
916
  * `length` can be an array (a length per bar, PineScript series length), also for `ta.lowest`, `ta.highestbars`
917
917
  * and `ta.lowestbars`: each bar uses its own length.
918
918
  * A length above 15,000 throws the PineScript runtime error (also `ta.lowest`, `ta.highestbars`, `ta.lowestbars`).
@@ -930,9 +930,9 @@ export function highest(source: Source, length: simple_int | ArrayLike<number>):
930
930
  * @returns Lowest value series (na on the first `length - 1` bars)
931
931
  *
932
932
  * @remarks
933
- * As in PineScript: `na` values are skipped. The result is the lowest of the last `length` non-na values (of all
934
- * of them when there are fewer), also on a bar whose own value is `na`; `na` before bar `length - 1` and while no
935
- * value was received.
933
+ * As in PineScript: `na` values (also +/-Infinity, #183) are skipped. The result is the lowest of the last `length`
934
+ * non-na values (of all of them when there are fewer), also on a bar whose own value is `na`; `na` before bar
935
+ * `length - 1` and while no value was received.
936
936
  */
937
937
  export function lowest(source: Source, length: simple_int | ArrayLike<number>): series_float {
938
938
  return extremeValue(source, length, true);
@@ -1018,7 +1018,7 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
1018
1018
  for (let i = 0; i < source.length; i++) {
1019
1019
  const v = source[i]!;
1020
1020
  const prev = i > 0 ? source[i - 1]! : NaN;
1021
- if (!Number.isNaN(v) && !Number.isNaN(prev)) count = step(v, prev) ? count + 1 : 0;
1021
+ if (Number.isFinite(v) && Number.isFinite(prev)) count = step(v, prev) ? count + 1 : 0;
1022
1022
  result.push(count >= length);
1023
1023
  }
1024
1024
  return result;
@@ -1035,8 +1035,8 @@ function monotonic(source: Source, length: simple_int, step: (newer: number, old
1035
1035
  * PineScript rules:
1036
1036
  * - a counter of rises from the previous bar (a rise is larger than the 1e-10 tolerance); a step that is not a rise
1037
1037
  * resets it to 0
1038
- * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
1039
- * result of the bar before the `na`)
1038
+ * - a step from or to an `na` (or +/-Infinity) bar leaves the counter unchanged (so an `na` bar, and the bar after
1039
+ * it, keep the result of the bar before the `na`)
1040
1040
  * - true when the counter is at least `length` (false at the start)
1041
1041
  *
1042
1042
  * @example
@@ -1061,8 +1061,8 @@ export function rising(source: Source, length: simple_int): series_bool {
1061
1061
  * PineScript rules:
1062
1062
  * - a counter of falls from the previous bar (a fall is larger than the 1e-10 tolerance); a step that is not a fall
1063
1063
  * resets it to 0
1064
- * - a step from or to an `na` bar leaves the counter unchanged (so an `na` bar, and the bar after it, keep the
1065
- * result of the bar before the `na`)
1064
+ * - a step from or to an `na` (or +/-Infinity) bar leaves the counter unchanged (so an `na` bar, and the bar after
1065
+ * it, keep the result of the bar before the `na`)
1066
1066
  * - true when the counter is at least `length` (false at the start)
1067
1067
  *
1068
1068
  * @example
@@ -1251,7 +1251,7 @@ function varianceOf(
1251
1251
  * @remarks
1252
1252
  * - Returns the middle value when values are sorted
1253
1253
  * - For an even length, returns the average of the two middle values
1254
- * - As in PineScript: `na` values are skipped, the window holds the last `length` non-na values
1254
+ * - As in PineScript: `na` values (also +/-Infinity) are skipped, the window holds the last `length` non-na values
1255
1255
  * (going back as many bars as needed), and a bar whose own value is `na` still gets a result
1256
1256
  * - Returns NaN until `length` non-na values exist
1257
1257
  * - More robust to outliers than mean (SMA)
@@ -1287,7 +1287,7 @@ export function median(source: Source, length: simple_int): series_float {
1287
1287
  const mid = Math.floor(len / 2);
1288
1288
  for (let i = 0; i < n; i++) {
1289
1289
  const x = source[i]!;
1290
- if (!Number.isNaN(x)) {
1290
+ if (Number.isFinite(x)) {
1291
1291
  kept[count++] = x;
1292
1292
  sorted.splice(bound(x, false), 0, x); // before the equal values: the newest first
1293
1293
  // the value leaving the window is the oldest, so the last of its equal values
@@ -1601,8 +1601,9 @@ export function cci(source: Source, length: simple_int): series_float {
1601
1601
  * - Measures where close is relative to the high-low range
1602
1602
  * - Values above 80 typically indicate overbought
1603
1603
  * - Values below 20 typically indicate oversold
1604
- * - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range is 0, the
1605
- * result is the previous result of the call (\`na\` until a first value)
1604
+ * - As PineScript: when the source, \`highest(high, length)\` or \`lowest(low, length)\` is \`na\`, or the range and
1605
+ * \`source - lowest\` are both 0, the result is the previous result of the call (\`na\` until a first value); a zero
1606
+ * range with another source value gives +/-Infinity (plotted as \`na\`)
1606
1607
  *
1607
1608
  * @example
1608
1609
  * \`\`\`typescript
@@ -1617,13 +1618,13 @@ export function stoch(source: Source, high: Source, low: Source, length: simple_
1617
1618
  const lowestValues = lowest(low, length);
1618
1619
  const highestValues = highest(high, length);
1619
1620
 
1620
- // PineScript: an na input or a zero range keeps the previous result
1621
+ // PineScript: an na (NaN) result keeps the previous one: an na input, or 0 / 0 on a zero range; a non-zero value
1622
+ // over a zero range gives +/-Infinity (#183)
1621
1623
  let previous = NaN;
1622
1624
  for (let i = 0; i < source.length; i++) {
1623
- const value = source[i]!;
1624
1625
  const lo = lowestValues[i]!;
1625
- const range = highestValues[i]! - lo;
1626
- if (!Number.isNaN(value) && !Number.isNaN(range) && range !== 0) previous = (100 * (value - lo)) / range;
1626
+ const k = (100 * (source[i]! - lo)) / (highestValues[i]! - lo);
1627
+ if (!Number.isNaN(k)) previous = k;
1627
1628
  result.push(previous);
1628
1629
  }
1629
1630
 
@@ -1898,8 +1899,8 @@ function pivot(source: Source, leftbars: PivotLength, rightbars: PivotLength, is
1898
1899
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1899
1900
  * index is `i - rightbars`
1900
1901
  * - A pivot high may equal values on its left but must be higher than all values on its right
1901
- * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the highest of
1902
- * them must be on the bar `rightbars` bars back
1902
+ * - `na` values (also +/-Infinity) are skipped: the window is the last `leftbars + rightbars + 1` non-na values,
1903
+ * and the highest of them must be on the bar `rightbars` bars back
1903
1904
  * - `leftbars` / `rightbars` can be arrays (a length per bar, PineScript series length), also for `ta.pivotlow`:
1904
1905
  * each bar uses its own lengths
1905
1906
  *
@@ -1938,8 +1939,8 @@ export function pivothigh(
1938
1939
  * - As in PineScript, the value appears `rightbars` bars after the pivot bar: the pivot bar
1939
1940
  * index is `i - rightbars`
1940
1941
  * - A pivot low may equal values on its left but must be lower than all values on its right
1941
- * - `na` values are skipped: the window is the last `leftbars + rightbars + 1` non-na values, and the lowest of
1942
- * them must be on the bar `rightbars` bars back
1942
+ * - `na` values (also +/-Infinity) are skipped: the window is the last `leftbars + rightbars + 1` non-na values,
1943
+ * and the lowest of them must be on the bar `rightbars` bars back
1943
1944
  *
1944
1945
  * @example
1945
1946
  * ```typescript
@@ -2529,7 +2530,7 @@ export function kcw(
2529
2530
  * (PineScript `ta.range(source, length)`).
2530
2531
  *
2531
2532
  * @remarks
2532
- * As in PineScript: `na` values are skipped (the window holds the last `length` non-na
2533
+ * As in PineScript: `na` values (also +/-Infinity) are skipped (the window holds the last `length` non-na
2533
2534
  * values), and a bar whose own value is `na` still gets a result.
2534
2535
  */
2535
2536
  export function range(source: Source, length: simple_int): series_float {
@@ -2545,9 +2546,9 @@ export function range(source: Source, length: simple_int): series_float {
2545
2546
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2546
2547
  *
2547
2548
  * @remarks
2548
- * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2549
- * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2550
- * `length - 1` and while no value was received; a fractional length is truncated.
2549
+ * As in PineScript: the best of the last `length` non-na values (+/-Infinity skipped), the newest one on equal
2550
+ * values; the offset is the distance in bars to its bar, so it is below `-(length - 1)` when `na` values were
2551
+ * skipped. `na` before bar `length - 1` and while no value was received; a fractional length is truncated.
2551
2552
  */
2552
2553
  export function highestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2553
2554
  return extremeOffset(source, length, false);
@@ -2558,9 +2559,9 @@ export function highestbars(source: Source, length: simple_int | ArrayLike<numbe
2558
2559
  * -1 for the previous bar, and so on (as in PineScript, the offset is negative).
2559
2560
  *
2560
2561
  * @remarks
2561
- * As in PineScript: the best of the last `length` non-na values, the newest one on equal values; the offset is the
2562
- * distance in bars to its bar, so it is below `-(length - 1)` when `na` values were skipped. `na` before bar
2563
- * `length - 1` and while no value was received; a fractional length is truncated.
2562
+ * As in PineScript: the best of the last `length` non-na values (+/-Infinity skipped), the newest one on equal
2563
+ * values; the offset is the distance in bars to its bar, so it is below `-(length - 1)` when `na` values were
2564
+ * skipped. `na` before bar `length - 1` and while no value was received; a fractional length is truncated.
2564
2565
  */
2565
2566
  export function lowestbars(source: Source, length: simple_int | ArrayLike<number>): series_int {
2566
2567
  return extremeOffset(source, length, true);
@@ -2570,13 +2571,13 @@ export function lowestbars(source: Source, length: simple_int | ArrayLike<number
2570
2571
  * All-time highest value of `source` up to each bar (PineScript `ta.max(source)`).
2571
2572
  *
2572
2573
  * @remarks
2573
- * As in PineScript: `na` values are skipped, the value carries over `na` bars, and the
2574
+ * As in PineScript: `na` values (also +/-Infinity) are skipped, the value carries over `na` bars, and the
2574
2575
  * result is `na` until the first non-na value.
2575
2576
  */
2576
2577
  export function max(source: Source): series_float {
2577
2578
  let best = NaN;
2578
2579
  return source.map((x) => {
2579
- if (!Number.isNaN(x) && !(x <= best)) best = x;
2580
+ if (Number.isFinite(x) && !(x <= best)) best = x;
2580
2581
  return best;
2581
2582
  });
2582
2583
  }
@@ -2585,13 +2586,13 @@ export function max(source: Source): series_float {
2585
2586
  * All-time lowest value of `source` up to each bar (PineScript `ta.min(source)`).
2586
2587
  *
2587
2588
  * @remarks
2588
- * As in PineScript: `na` values are skipped, the value carries over `na` bars, and the
2589
+ * As in PineScript: `na` values (also +/-Infinity) are skipped, the value carries over `na` bars, and the
2589
2590
  * result is `na` until the first non-na value.
2590
2591
  */
2591
2592
  export function min(source: Source): series_float {
2592
2593
  let best = NaN;
2593
2594
  return source.map((x) => {
2594
- if (!Number.isNaN(x) && !(x >= best)) best = x;
2595
+ if (Number.isFinite(x) && !(x >= best)) best = x;
2595
2596
  return best;
2596
2597
  });
2597
2598
  }
@@ -2647,8 +2648,8 @@ export function cog(source: Source, length: simple_int = 10): series_float {
2647
2648
  *
2648
2649
  * @remarks
2649
2650
  * PineScript rules:
2650
- * - the window is the last `length` non-`na` values (an `na` bar does not enter it, and gets the mode of the
2651
- * window it keeps)
2651
+ * - the window is the last `length` non-`na` values (an `na` or +/-Infinity bar does not enter it, and gets the mode
2652
+ * of the window it keeps)
2652
2653
  * - NaN until `length` non-`na` values exist
2653
2654
  * - If no mode exists, returns the smallest value
2654
2655
  */
@@ -2660,7 +2661,7 @@ export function mode(source: Source, length: simple_int): series_float {
2660
2661
 
2661
2662
  for (let i = 0; i < source.length; i++) {
2662
2663
  const v = source[i]!;
2663
- if (!Number.isNaN(v)) seen.push(v);
2664
+ if (Number.isFinite(v)) seen.push(v);
2664
2665
  if (seen.length < len) {
2665
2666
  result.push(NaN);
2666
2667
  continue;
@@ -12,7 +12,9 @@
12
12
  * - `table.cell_set_*` changes one property of a cell (a cell that was not defined is created)
13
13
  * - a column or row outside the table is a runtime error
14
14
  * - a deleted table is `na`: its functions do nothing
15
- * - one live table per position: `table.new` at the position of a live table deletes that table (#176)
15
+ * - one live table per position and pane: `table.new` or `table.set_position` at the position of a live table with
16
+ * the same `force_overlay` deletes that table (#176, #182); a table with `force_overlay = true` and one without
17
+ * keep each their own position, also in an overlay script
16
18
  *
17
19
  * @version 6
18
20
  */
@@ -25,9 +27,16 @@ type HAlign = 'left' | 'center' | 'right';
25
27
  type VAlign = 'top' | 'center' | 'bottom';
26
28
  type FontFamily = 'default' | 'monospace';
27
29
 
30
+ /** Deletes the live tables other than `keep` at `position` with the same `force_overlay` (#176, #182). */
31
+ function freePosition(position: TablePosition, force_overlay: boolean, keep?: Table): void {
32
+ for (const old of allDrawings('table')) {
33
+ if (old !== keep && old.position === position && (old.force_overlay ?? false) === force_overlay) remove('table', old);
34
+ }
35
+ }
36
+
28
37
  /**
29
- * Creates a new table (PineScript `table.new`). A live table at the same position is deleted, as PineScript keeps
30
- * one table per position (#176).
38
+ * Creates a new table (PineScript `table.new`). A live table at the same position with the same `force_overlay` is
39
+ * deleted, as PineScript keeps one table per position and pane (#176, #182).
31
40
  *
32
41
  * @example
33
42
  * ```typescript
@@ -46,7 +55,7 @@ export function new_table(
46
55
  border_width?: number,
47
56
  force_overlay?: boolean
48
57
  ): Table {
49
- for (const old of allDrawings('table')) if (old.position === position) remove('table', old);
58
+ freePosition(position, force_overlay ?? false);
50
59
  return register('table', {
51
60
  position,
52
61
  columns,
@@ -196,8 +205,15 @@ function setTable<K extends keyof Table>(field: K) {
196
205
  };
197
206
  }
198
207
 
199
- /** Moves the table to another position (`position.*`). */
200
- export const set_position = setTable('position');
208
+ /**
209
+ * Moves the table to another position (`position.*`). A live table at that position with the same `force_overlay` is
210
+ * deleted (#182).
211
+ */
212
+ export function set_position(table_id: Table, position: TablePosition): void {
213
+ if (isDeleted(table_id)) return;
214
+ freePosition(position, table_id.force_overlay ?? false, table_id);
215
+ table_id.position = position;
216
+ }
201
217
  /** Sets the background colour of the table. */
202
218
  export const set_bgcolor = setTable('bgcolor');
203
219
  /** Sets the colour of the outer frame. */