oakscriptjs 0.11.0 → 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 (238) hide show
  1. package/dist/cjs/array/index.d.ts +8 -2
  2. package/dist/cjs/array/index.d.ts.map +1 -1
  3. package/dist/cjs/array/index.js +2 -1
  4. package/dist/cjs/array/index.js.map +2 -2
  5. package/dist/cjs/box/index.js +6 -6
  6. package/dist/cjs/box/index.js.map +2 -2
  7. package/dist/cjs/callsite/index.d.ts.map +1 -1
  8. package/dist/cjs/callsite/index.js +6 -3
  9. package/dist/cjs/callsite/index.js.map +2 -2
  10. package/dist/cjs/drawing/registry.d.ts +15 -6
  11. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  12. package/dist/cjs/drawing/registry.js +16 -2
  13. package/dist/cjs/drawing/registry.js.map +2 -2
  14. package/dist/cjs/index.d.ts +4 -3
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +7 -1
  17. package/dist/cjs/index.js.map +2 -2
  18. package/dist/cjs/input.d.ts +9 -1
  19. package/dist/cjs/input.d.ts.map +1 -1
  20. package/dist/cjs/input.js +16 -0
  21. package/dist/cjs/input.js.map +2 -2
  22. package/dist/cjs/label/index.js +3 -3
  23. package/dist/cjs/label/index.js.map +2 -2
  24. package/dist/cjs/line/index.d.ts +10 -27
  25. package/dist/cjs/line/index.d.ts.map +1 -1
  26. package/dist/cjs/line/index.js +10 -27
  27. package/dist/cjs/line/index.js.map +2 -2
  28. package/dist/cjs/linefill/index.d.ts +3 -0
  29. package/dist/cjs/linefill/index.d.ts.map +1 -1
  30. package/dist/cjs/linefill/index.js +15 -1
  31. package/dist/cjs/linefill/index.js.map +3 -3
  32. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js +1 -1
  33. package/dist/cjs/runtime/adapters/SimpleInputAdapter.js.map +2 -2
  34. package/dist/cjs/runtime/index.d.ts +1 -1
  35. package/dist/cjs/runtime/index.d.ts.map +1 -1
  36. package/dist/cjs/runtime/index.js +1 -0
  37. package/dist/cjs/runtime/index.js.map +2 -2
  38. package/dist/cjs/runtime/inputs.d.ts +9 -0
  39. package/dist/cjs/runtime/inputs.d.ts.map +1 -1
  40. package/dist/cjs/runtime/inputs.js +8 -0
  41. package/dist/cjs/runtime/inputs.js.map +2 -2
  42. package/dist/cjs/runtime/types.d.ts +1 -1
  43. package/dist/cjs/runtime/types.d.ts.map +1 -1
  44. package/dist/cjs/runtime/types.js.map +1 -1
  45. package/dist/cjs/script/index.d.ts +66 -13
  46. package/dist/cjs/script/index.d.ts.map +1 -1
  47. package/dist/cjs/script/index.js +139 -35
  48. package/dist/cjs/script/index.js.map +3 -3
  49. package/dist/cjs/security/other.d.ts +92 -0
  50. package/dist/cjs/security/other.d.ts.map +1 -0
  51. package/dist/cjs/security/other.js +164 -0
  52. package/dist/cjs/security/other.js.map +7 -0
  53. package/dist/cjs/session/bars.d.ts +10 -1
  54. package/dist/cjs/session/bars.d.ts.map +1 -1
  55. package/dist/cjs/session/bars.js +16 -2
  56. package/dist/cjs/session/bars.js.map +2 -2
  57. package/dist/cjs/session/calendar.d.ts +4 -1
  58. package/dist/cjs/session/calendar.d.ts.map +1 -1
  59. package/dist/cjs/session/calendar.js +12 -7
  60. package/dist/cjs/session/calendar.js.map +2 -2
  61. package/dist/cjs/session/context.d.ts +22 -5
  62. package/dist/cjs/session/context.d.ts.map +1 -1
  63. package/dist/cjs/session/context.js +67 -6
  64. package/dist/cjs/session/context.js.map +2 -2
  65. package/dist/cjs/str/floatingdecimal.d.ts +31 -0
  66. package/dist/cjs/str/floatingdecimal.d.ts.map +1 -0
  67. package/dist/cjs/str/floatingdecimal.js +298 -0
  68. package/dist/cjs/str/floatingdecimal.js.map +7 -0
  69. package/dist/cjs/str/index.d.ts +4 -3
  70. package/dist/cjs/str/index.d.ts.map +1 -1
  71. package/dist/cjs/str/index.js +1 -1
  72. package/dist/cjs/str/index.js.map +2 -2
  73. package/dist/cjs/str/numberformat.d.ts +6 -4
  74. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  75. package/dist/cjs/str/numberformat.js +3 -2
  76. package/dist/cjs/str/numberformat.js.map +2 -2
  77. package/dist/cjs/ta/index.d.ts +64 -45
  78. package/dist/cjs/ta/index.d.ts.map +1 -1
  79. package/dist/cjs/ta/index.js +140 -126
  80. package/dist/cjs/ta/index.js.map +3 -3
  81. package/dist/cjs/ta/limits.d.ts +11 -0
  82. package/dist/cjs/ta/limits.d.ts.map +1 -1
  83. package/dist/cjs/ta/limits.js +15 -1
  84. package/dist/cjs/ta/limits.js.map +2 -2
  85. package/dist/cjs/ta/running-sum.js +1 -1
  86. package/dist/cjs/ta/running-sum.js.map +2 -2
  87. package/dist/cjs/ta-series.d.ts +1 -1
  88. package/dist/cjs/ta-series.d.ts.map +1 -1
  89. package/dist/cjs/ta-series.js +1 -1
  90. package/dist/cjs/ta-series.js.map +2 -2
  91. package/dist/cjs/table/index.d.ts +10 -3
  92. package/dist/cjs/table/index.d.ts.map +1 -1
  93. package/dist/cjs/table/index.js +11 -1
  94. package/dist/cjs/table/index.js.map +3 -3
  95. package/dist/cjs/time/session.d.ts +9 -0
  96. package/dist/cjs/time/session.d.ts.map +1 -1
  97. package/dist/cjs/time/session.js +42 -14
  98. package/dist/cjs/time/session.js.map +3 -3
  99. package/dist/cjs/time/timezone.d.ts +6 -1
  100. package/dist/cjs/time/timezone.d.ts.map +1 -1
  101. package/dist/cjs/time/timezone.js +2 -2
  102. package/dist/cjs/time/timezone.js.map +2 -2
  103. package/dist/cjs/types/metadata.d.ts +1 -1
  104. package/dist/cjs/types/metadata.d.ts.map +1 -1
  105. package/dist/cjs/types/metadata.js.map +1 -1
  106. package/dist/esm/array/index.d.ts +8 -2
  107. package/dist/esm/array/index.d.ts.map +1 -1
  108. package/dist/esm/array/index.js +12 -3
  109. package/dist/esm/array/index.js.map +1 -1
  110. package/dist/esm/box/index.js +6 -6
  111. package/dist/esm/box/index.js.map +1 -1
  112. package/dist/esm/callsite/index.d.ts.map +1 -1
  113. package/dist/esm/callsite/index.js +6 -3
  114. package/dist/esm/callsite/index.js.map +1 -1
  115. package/dist/esm/drawing/registry.d.ts +15 -6
  116. package/dist/esm/drawing/registry.d.ts.map +1 -1
  117. package/dist/esm/drawing/registry.js +30 -7
  118. package/dist/esm/drawing/registry.js.map +1 -1
  119. package/dist/esm/index.d.ts +4 -3
  120. package/dist/esm/index.d.ts.map +1 -1
  121. package/dist/esm/index.js +8 -5
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/input.d.ts +9 -1
  124. package/dist/esm/input.d.ts.map +1 -1
  125. package/dist/esm/input.js +16 -0
  126. package/dist/esm/input.js.map +1 -1
  127. package/dist/esm/label/index.js +3 -3
  128. package/dist/esm/label/index.js.map +1 -1
  129. package/dist/esm/line/index.d.ts +10 -27
  130. package/dist/esm/line/index.d.ts.map +1 -1
  131. package/dist/esm/line/index.js +20 -64
  132. package/dist/esm/line/index.js.map +1 -1
  133. package/dist/esm/linefill/index.d.ts +3 -0
  134. package/dist/esm/linefill/index.d.ts.map +1 -1
  135. package/dist/esm/linefill/index.js +20 -1
  136. package/dist/esm/linefill/index.js.map +1 -1
  137. package/dist/esm/runtime/adapters/SimpleInputAdapter.js +1 -1
  138. package/dist/esm/runtime/adapters/SimpleInputAdapter.js.map +1 -1
  139. package/dist/esm/runtime/index.d.ts +1 -1
  140. package/dist/esm/runtime/index.d.ts.map +1 -1
  141. package/dist/esm/runtime/index.js +1 -1
  142. package/dist/esm/runtime/index.js.map +1 -1
  143. package/dist/esm/runtime/inputs.d.ts +9 -0
  144. package/dist/esm/runtime/inputs.d.ts.map +1 -1
  145. package/dist/esm/runtime/inputs.js +15 -0
  146. package/dist/esm/runtime/inputs.js.map +1 -1
  147. package/dist/esm/runtime/types.d.ts +1 -1
  148. package/dist/esm/runtime/types.d.ts.map +1 -1
  149. package/dist/esm/script/index.d.ts +66 -13
  150. package/dist/esm/script/index.d.ts.map +1 -1
  151. package/dist/esm/script/index.js +187 -54
  152. package/dist/esm/script/index.js.map +1 -1
  153. package/dist/esm/security/other.d.ts +92 -0
  154. package/dist/esm/security/other.d.ts.map +1 -0
  155. package/dist/esm/security/other.js +211 -0
  156. package/dist/esm/security/other.js.map +1 -0
  157. package/dist/esm/session/bars.d.ts +10 -1
  158. package/dist/esm/session/bars.d.ts.map +1 -1
  159. package/dist/esm/session/bars.js +26 -1
  160. package/dist/esm/session/bars.js.map +1 -1
  161. package/dist/esm/session/calendar.d.ts +4 -1
  162. package/dist/esm/session/calendar.d.ts.map +1 -1
  163. package/dist/esm/session/calendar.js +23 -7
  164. package/dist/esm/session/calendar.js.map +1 -1
  165. package/dist/esm/session/context.d.ts +22 -5
  166. package/dist/esm/session/context.d.ts.map +1 -1
  167. package/dist/esm/session/context.js +86 -10
  168. package/dist/esm/session/context.js.map +1 -1
  169. package/dist/esm/str/floatingdecimal.d.ts +31 -0
  170. package/dist/esm/str/floatingdecimal.d.ts.map +1 -0
  171. package/dist/esm/str/floatingdecimal.js +272 -0
  172. package/dist/esm/str/floatingdecimal.js.map +1 -0
  173. package/dist/esm/str/index.d.ts +4 -3
  174. package/dist/esm/str/index.d.ts.map +1 -1
  175. package/dist/esm/str/index.js +6 -5
  176. package/dist/esm/str/index.js.map +1 -1
  177. package/dist/esm/str/numberformat.d.ts +6 -4
  178. package/dist/esm/str/numberformat.d.ts.map +1 -1
  179. package/dist/esm/str/numberformat.js +9 -5
  180. package/dist/esm/str/numberformat.js.map +1 -1
  181. package/dist/esm/ta/index.d.ts +64 -45
  182. package/dist/esm/ta/index.d.ts.map +1 -1
  183. package/dist/esm/ta/index.js +198 -228
  184. package/dist/esm/ta/index.js.map +1 -1
  185. package/dist/esm/ta/limits.d.ts +11 -0
  186. package/dist/esm/ta/limits.d.ts.map +1 -1
  187. package/dist/esm/ta/limits.js +27 -2
  188. package/dist/esm/ta/limits.js.map +1 -1
  189. package/dist/esm/ta/running-sum.js +1 -1
  190. package/dist/esm/ta/running-sum.js.map +1 -1
  191. package/dist/esm/ta-series.d.ts +1 -1
  192. package/dist/esm/ta-series.d.ts.map +1 -1
  193. package/dist/esm/ta-series.js +2 -1
  194. package/dist/esm/ta-series.js.map +1 -1
  195. package/dist/esm/table/index.d.ts +10 -3
  196. package/dist/esm/table/index.d.ts.map +1 -1
  197. package/dist/esm/table/index.js +23 -3
  198. package/dist/esm/table/index.js.map +1 -1
  199. package/dist/esm/time/session.d.ts +9 -0
  200. package/dist/esm/time/session.d.ts.map +1 -1
  201. package/dist/esm/time/session.js +53 -15
  202. package/dist/esm/time/session.js.map +1 -1
  203. package/dist/esm/time/timezone.d.ts +6 -1
  204. package/dist/esm/time/timezone.d.ts.map +1 -1
  205. package/dist/esm/time/timezone.js +8 -2
  206. package/dist/esm/time/timezone.js.map +1 -1
  207. package/dist/esm/types/metadata.d.ts +1 -1
  208. package/dist/esm/types/metadata.d.ts.map +1 -1
  209. package/package.json +1 -1
  210. package/src/array/index.ts +11 -3
  211. package/src/box/index.ts +6 -6
  212. package/src/callsite/index.ts +6 -3
  213. package/src/drawing/registry.ts +30 -7
  214. package/src/index.ts +16 -3
  215. package/src/input.ts +18 -1
  216. package/src/label/index.ts +3 -3
  217. package/src/line/index.ts +20 -68
  218. package/src/linefill/index.ts +20 -1
  219. package/src/runtime/adapters/SimpleInputAdapter.ts +1 -1
  220. package/src/runtime/index.ts +1 -0
  221. package/src/runtime/inputs.ts +16 -0
  222. package/src/runtime/types.ts +1 -1
  223. package/src/script/index.ts +262 -58
  224. package/src/security/other.ts +269 -0
  225. package/src/session/bars.ts +24 -1
  226. package/src/session/calendar.ts +26 -7
  227. package/src/session/context.ts +104 -10
  228. package/src/str/floatingdecimal.ts +268 -0
  229. package/src/str/index.ts +6 -5
  230. package/src/str/numberformat.ts +11 -6
  231. package/src/ta/index.ts +200 -225
  232. package/src/ta/limits.ts +28 -1
  233. package/src/ta/running-sum.ts +1 -1
  234. package/src/ta-series.ts +3 -2
  235. package/src/table/index.ts +22 -3
  236. package/src/time/session.ts +58 -16
  237. package/src/time/timezone.ts +9 -2
  238. package/src/types/metadata.ts +1 -1
package/src/ta/limits.ts CHANGED
@@ -37,11 +37,38 @@ export function windowError(name: string, length: number, bar?: number, max = MA
37
37
  /** Throws RE10004 when the (truncated) length of a window function is above the limit. */
38
38
  export function checkWindowLength(name: string, length: number, bar?: number, max = MAX_WINDOW_LENGTH): void {
39
39
  const len = Math.trunc(length);
40
+ if (len <= 0) throw new Error(lengthError(name, len, bar));
40
41
  if (len > max) throw new Error(windowError(name, len, bar, max));
41
42
  }
42
43
 
43
44
  /** Throws RE10004 for a function of {@link SUM_FUNCTIONS} ("ta.sma", ...) with a length above 5,000. */
44
45
  export function checkSumLength(name: string, length: number, bar?: number): void {
45
46
  const fn = SUM_FUNCTIONS[name];
46
- if (fn !== undefined) checkWindowLength(fn, length, bar, MAX_SUM_LENGTH);
47
+ if (fn === undefined) return;
48
+ // RE10001 names 'bb' itself, RE10004 its inner 'sma'
49
+ checkPositiveLength(name === 'ta.bb' ? 'bb' : fn, length, bar);
50
+ checkWindowLength(fn, length, bar, MAX_SUM_LENGTH);
51
+ }
52
+
53
+ /**
54
+ * PineScript's runtime error RE10001 text for a length that is 0 or negative. `name` is the function, or a call path
55
+ * "atr->rma" (printed 'atr'->'rma'); the value prints as a float ("0.0", "-1.0").
56
+ */
57
+ export function lengthError(name: string, value: number, bar?: number, arg = 'length'): string {
58
+ const fn = name.split('->').map((part) => `'${part}'`).join('->');
59
+ const text = Number.isInteger(value) ? value.toFixed(1) : String(value);
60
+ return (
61
+ (bar === undefined ? '' : `Error on bar ${bar}: `) +
62
+ `Invalid value of the '${arg}' argument (${text}) in the ${fn} function. It must be > 0.`
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Throws RE10001 when the (truncated) length is 0 or negative, as PineScript does for every window function on the
68
+ * bar of the call (#175): `ta.sma(close, 0)` stops on bar 0, a series length that becomes 0 on the bar where it
69
+ * does. `ta.hma(src, 1)` stops in its inner `wma(src, 0)`. An na length is not checked here.
70
+ */
71
+ export function checkPositiveLength(name: string, length: number, bar?: number, arg = 'length'): void {
72
+ const len = Math.trunc(length);
73
+ if (len <= 0) throw new Error(lengthError(name, len, bar, arg));
47
74
  }
@@ -177,8 +177,8 @@ function seriesLengths(lengths: ArrayLike<number>, count: number, name: string):
177
177
  }
178
178
  return Array.from({ length: count }, (_, i) => {
179
179
  const len = Math.floor(lengths[i]!);
180
- if (!(len >= 1)) throw new Error(`${name}: length must be at least 1 (bar ${i}: ${lengths[i]})`);
181
180
  checkSumLength(name, len, i);
181
+ if (!(len >= 1)) throw new Error(`${name}: length must be at least 1 (bar ${i}: ${lengths[i]})`);
182
182
  return len;
183
183
  });
184
184
  }
package/src/ta-series.ts CHANGED
@@ -66,10 +66,11 @@ export function rma(source: Series, length: number): Series {
66
66
  * @param length - Period length
67
67
  * @returns Series with RSI values
68
68
  */
69
- export function rsi(source: Series, length: number): Series {
69
+ export function rsi(source: Series, length: number | Series): Series {
70
70
  const bars = source.bars as Bar[];
71
71
  const sourceValues = source.toArray();
72
- const result = taCore.rsi(sourceValues, length);
72
+ // a Series as second argument: PineScript v4 rsi(x, y) of two averages (#180)
73
+ const result = length instanceof Series ? taCore.rsi(sourceValues, length.toArray()) : taCore.rsi(sourceValues, length);
73
74
  return Series.fromArray(bars, result);
74
75
  }
75
76
 
@@ -12,6 +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 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
15
18
  *
16
19
  * @version 6
17
20
  */
@@ -24,8 +27,16 @@ type HAlign = 'left' | 'center' | 'right';
24
27
  type VAlign = 'top' | 'center' | 'bottom';
25
28
  type FontFamily = 'default' | 'monospace';
26
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
+
27
37
  /**
28
- * Creates a new table (PineScript `table.new`).
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).
29
40
  *
30
41
  * @example
31
42
  * ```typescript
@@ -44,6 +55,7 @@ export function new_table(
44
55
  border_width?: number,
45
56
  force_overlay?: boolean
46
57
  ): Table {
58
+ freePosition(position, force_overlay ?? false);
47
59
  return register('table', {
48
60
  position,
49
61
  columns,
@@ -193,8 +205,15 @@ function setTable<K extends keyof Table>(field: K) {
193
205
  };
194
206
  }
195
207
 
196
- /** Moves the table to another position (`position.*`). */
197
- 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
+ }
198
217
  /** Sets the background colour of the table. */
199
218
  export const set_bgcolor = setTable('bgcolor');
200
219
  /** Sets the colour of the outer frame. */
@@ -14,11 +14,13 @@
14
14
  * "0000-0000" and "24x7" are the whole day, every day
15
15
  * - a run that ends at 0000 ("2000-0000") ends at 24:00 of the day it starts and belongs to that day
16
16
  * ("2000-0000:6" is Friday 20:00 to midnight)
17
+ * - "F" after the start moves it one day before the day the run ends on ("1700F-1900:2": Sunday 17:00 to Monday
18
+ * 19:00; "1900-1900:3456|1700F-1900:2" is the TVC:DXY symbol session) (#173)
17
19
  * - a string that is empty or does not start with a digit ("regular", "extended") is the symbol's own session
18
20
  * - a malformed string ("1100 -1400", "0930-16", ":8") is an error, as the PineScript runtime error
19
21
  */
20
22
 
21
- import { zoneOffset, zonedFields } from './timezone.js';
23
+ import { zoneOffset, zonedFields, zonedToUnix } from './timezone.js';
22
24
 
23
25
  const MINUTES_PER_DAY = 1440;
24
26
  const WEEKDAYS = [2, 3, 4, 5, 6];
@@ -29,17 +31,27 @@ export interface SessionRange {
29
31
  start: number;
30
32
  end: number;
31
33
  days: Set<number>;
34
+ /** Days between the start and the day the run ends on, given by "F" after the start (absent otherwise) */
35
+ startDays?: number;
32
36
  }
33
37
 
34
38
  function invalid(session: string): SyntaxError {
35
39
  return new SyntaxError(`Invalid session "${session}": expected "HHMM-HHMM[,HHMM-HHMM...][:days][|...]"`);
36
40
  }
37
41
 
38
- function parseRanges(text: string, session: string): Array<[number, number]> {
42
+ function parseRanges(text: string, session: string): Array<[number, number, number?]> {
39
43
  if (!text) return [];
40
44
  const entries = text.split(',');
41
45
  if (entries.length > 1 && entries[entries.length - 1] === '') entries.pop();
42
- return entries.map((entry) => {
46
+ return entries.map((entry): [number, number, number?] => {
47
+ // "HHMMF-HHMM": the start is one day before the day the run ends on
48
+ const f = /^(\d{4})F-(\d{4})$/.exec(entry);
49
+ if (f) {
50
+ const minutes = (hhmm: string): number => Number(hhmm.slice(0, 2)) * 60 + Number(hhmm.slice(2, 4));
51
+ const [start, end] = [minutes(f[1]!), minutes(f[2]!)];
52
+ if (start >= MINUTES_PER_DAY || end >= MINUTES_PER_DAY) throw invalid(session);
53
+ return [start, end, 1];
54
+ }
43
55
  if (!/^\d{4}-\d{4}$/.test(entry)) throw invalid(session);
44
56
  let start = Number(entry.slice(0, 2)) * 60 + Number(entry.slice(2, 4));
45
57
  let end = Number(entry.slice(5, 7)) * 60 + Number(entry.slice(7, 9));
@@ -60,6 +72,9 @@ function parseDays(text: string, session: string): Set<number> {
60
72
 
61
73
  const parsed = new Map<string, SessionRange[] | null>();
62
74
 
75
+ const range = (start: number, end: number, days: Set<number>, startDays?: number): SessionRange =>
76
+ startDays === undefined ? { start, end, days } : { start, end, days, startDays };
77
+
63
78
  /**
64
79
  * Runs of a PineScript session string, or null for the symbol's own session (empty or not starting with a digit).
65
80
  * @throws SyntaxError when the string is malformed (a PineScript runtime error)
@@ -74,7 +89,7 @@ export function parsePineSession(session: string): SessionRange[] | null {
74
89
  } else if (spec === '24x7') {
75
90
  out = [{ start: 0, end: 0, days: new Set(ALL_DAYS) }];
76
91
  } else if (!/[:,|]/.test(spec)) {
77
- out = parseRanges(spec, session).map(([start, end]) => ({ start, end, days: new Set(ALL_DAYS) }));
92
+ out = parseRanges(spec, session).map(([start, end, startDays]) => range(start, end, new Set(ALL_DAYS), startDays));
78
93
  } else {
79
94
  const sections = spec.split('|');
80
95
  if (sections.length > 1 && sections[sections.length - 1] === '') sections.pop();
@@ -99,7 +114,7 @@ export function parsePineSession(session: string): SessionRange[] | null {
99
114
  items.forEach((it, i) => {
100
115
  if (it.days === null) for (const d of WEEKDAYS) if (!claimed.has(d)) own[i]!.add(d);
101
116
  });
102
- out = items.flatMap((it, i) => (own[i]!.size ? it.ranges.map(([start, end]) => ({ start, end, days: own[i]! })) : []));
117
+ out = items.flatMap((it, i) => (own[i]!.size ? it.ranges.map(([start, end, startDays]) => range(start, end, own[i]!, startDays)) : []));
103
118
  }
104
119
  parsed.set(session, out);
105
120
  return out;
@@ -147,29 +162,56 @@ function clockChangeNear(time: number, timezone: string): { at: number; before:
147
162
  * `time(timeframe, session)` in a script with the chart session
148
163
  */
149
164
  export function inSession(time: number, session: string, timezone: string, barMs?: number): boolean {
150
- if (Number.isNaN(time)) return false;
165
+ return sessionRun(time, session, timezone, barMs) !== null;
166
+ }
167
+
168
+ /**
169
+ * The run of `session` that contains `time`, as [start, end) UNIX times (ms), or null outside the session; rules in
170
+ * {@link inSession}. PineScript aligns the intraday periods of `time(tf, session)` on the start of this run (#173).
171
+ */
172
+ export function sessionRun(time: number, session: string, timezone: string, barMs?: number): [number, number] | null {
173
+ if (Number.isNaN(time)) return null;
151
174
  const ranges = parsePineSession(session);
152
175
  if (ranges === null) throw new Error(`Session "${session}" is the symbol session: it needs the chart session (time(timeframe, session) in a script)`);
153
176
  const change = clockChangeNear(time, timezone);
154
177
  const f = zonedFields(time, timezone);
155
178
  const minute = f.hour * 60 + f.minute;
156
179
  const nextDay = (f.dayOfWeek % 7) + 1;
157
- for (const { start, end: rawEnd, days } of ranges) {
180
+ for (const { start, end: rawEnd, days, startDays } of ranges) {
181
+ if (startDays !== undefined) {
182
+ // "F": from `start` of `startDays` days before the end day to `end` of the end day, wall times read in the zone
183
+ for (let k = 0; k <= startDays + 1; k++) {
184
+ const endDay = Date.UTC(f.year, f.month - 1, f.day + k);
185
+ if (!days.has(new Date(endDay).getUTCDay() + 1)) continue;
186
+ const d = new Date(endDay);
187
+ const at = (dayShift: number, minutes: number): number =>
188
+ zonedToUnix(timezone, d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() - dayShift, Math.floor(minutes / 60), minutes % 60);
189
+ const from = at(startDays, start);
190
+ const to = at(0, rawEnd);
191
+ if (time >= from && time < to) return [from, to];
192
+ }
193
+ continue;
194
+ }
158
195
  if (start === rawEnd) {
159
196
  // 24 hours: the run belongs to the day it ends ("0000-0000": the day itself)
160
- if (days.has(start === 0 || minute < start ? f.dayOfWeek : nextDay)) return true;
197
+ if (days.has(start === 0 || minute < start ? f.dayOfWeek : nextDay)) {
198
+ const from = time - ((minute - start + MINUTES_PER_DAY) % MINUTES_PER_DAY) * MS_PER_MINUTE - f.second * 1000 - (time % 1000);
199
+ return [from, from + MS_PER_DAY];
200
+ }
161
201
  continue;
162
202
  }
163
203
  // a run that ends at 0000 ends at 24:00 of the day it starts
164
204
  const end = rawEnd === 0 ? MINUTES_PER_DAY : rawEnd;
165
205
  const overnight = start > end;
166
206
  if (!change) {
207
+ // the wall-clock minute of the bar: the run is `start` to `end` minutes of the same offset
208
+ const midnight = time - minute * MS_PER_MINUTE - f.second * 1000 - (time % 1000);
167
209
  if (!overnight) {
168
- if (minute >= start && minute < end && days.has(f.dayOfWeek)) return true;
210
+ if (minute >= start && minute < end && days.has(f.dayOfWeek)) return [midnight + start * MS_PER_MINUTE, midnight + end * MS_PER_MINUTE];
169
211
  } else {
170
212
  // the part from `start` belongs to the next day
171
- if (minute >= start && days.has(nextDay)) return true;
172
- if (minute < end && days.has(f.dayOfWeek)) return true;
213
+ if (minute >= start && days.has(nextDay)) return [midnight + start * MS_PER_MINUTE, midnight + (end + MINUTES_PER_DAY) * MS_PER_MINUTE];
214
+ if (minute < end && days.has(f.dayOfWeek)) return [midnight + (start - MINUTES_PER_DAY) * MS_PER_MINUTE, midnight + end * MS_PER_MINUTE];
173
215
  }
174
216
  continue;
175
217
  }
@@ -182,14 +224,14 @@ export function inSession(time: number, session: string, timezone: string, barMs
182
224
  const from = day * MS_PER_DAY + (overnight ? start - MINUTES_PER_DAY : start) * MS_PER_MINUTE;
183
225
  const to = day * MS_PER_DAY + end * MS_PER_MINUTE;
184
226
  if (to === changeWall) {
185
- if (time >= from - change.after && time < to - change.after) return true;
227
+ if (time >= from - change.after && time < to - change.after) return [from - change.after, to - change.after];
186
228
  continue;
187
229
  }
188
- if (time >= place(from) && time < place(to)) return true;
230
+ if (time >= place(from) && time < place(to)) return [place(from), place(to)];
189
231
  // clocks going back: the second pass of the repeated hour
190
232
  const repeated = change.at + change.after;
191
233
  if (change.after < change.before && to < changeWall && to > repeated) {
192
- if (time >= Math.max(from, repeated) - change.after && time < to - change.after) return true;
234
+ if (time >= Math.max(from, repeated) - change.after && time < to - change.after) return [place(from), to - change.after];
193
235
  }
194
236
  }
195
237
  // clocks going back, the day before: a run with an end on the wall time of the change is also true one change
@@ -201,9 +243,9 @@ export function inSession(time: number, session: string, timezone: string, barMs
201
243
  const from = day * MS_PER_DAY + (overnight ? start - MINUTES_PER_DAY : start) * MS_PER_MINUTE - change.before;
202
244
  const to = day * MS_PER_DAY + end * MS_PER_MINUTE - change.before;
203
245
  const shift = change.before - change.after;
204
- if (time >= from + shift && time < to + shift && time >= to + barMs) return true;
246
+ if (time >= from + shift && time < to + shift && time >= to + barMs) return [from + shift, to + shift];
205
247
  }
206
248
  }
207
249
  }
208
- return false;
250
+ return null;
209
251
  }
@@ -177,6 +177,11 @@ export function civilToUnix(year: number, month: number, day: number, hour = 0,
177
177
  return days * MS_PER_DAY + Date.UTC(1970, 0, 1, hour, minute, second);
178
178
  }
179
179
 
180
+ /**
181
+ * UNIX time (ms) of a wall-clock date in `timezone`. A wall time skipped by a clock change is read with the offset
182
+ * before the change (02:30 on a 02:00 -> 03:00 night is 03:30 after it); a wall time that occurs twice gives the
183
+ * later instant, or the earlier one with `earlier`.
184
+ */
180
185
  export function zonedToUnix(
181
186
  timezone: string,
182
187
  year: number,
@@ -184,7 +189,8 @@ export function zonedToUnix(
184
189
  day: number,
185
190
  hour: number = 0,
186
191
  minute: number = 0,
187
- second: number = 0
192
+ second: number = 0,
193
+ earlier: boolean = false
188
194
  ): number {
189
195
  const zone = resolveZone(timezone);
190
196
  const local = civilToUnix(year, month, day, hour, minute, second);
@@ -195,6 +201,7 @@ export function zonedToUnix(
195
201
  const before = offsetAt(zone, local - MS_PER_DAY);
196
202
  const after = offsetAt(zone, local + MS_PER_DAY);
197
203
  const candidates = [local - before, local - after].filter((t) => local - offsetAt(zone, t) === t);
198
- if (candidates.length) return Math.max(...candidates);
204
+ // a wall time that occurs twice: the later instant, or the earlier one with `earlier`
205
+ if (candidates.length) return earlier ? Math.min(...candidates) : Math.max(...candidates);
199
206
  return local - before;
200
207
  }
@@ -86,7 +86,7 @@ export interface FillOptions {
86
86
  /**
87
87
  * Input parameter type
88
88
  */
89
- export type InputType = 'int' | 'float' | 'bool' | 'string' | 'source' | 'color' | 'timeframe' | 'session' | 'time';
89
+ export type InputType = 'int' | 'float' | 'bool' | 'string' | 'source' | 'color' | 'timeframe' | 'session' | 'time' | 'symbol';
90
90
 
91
91
  /**
92
92
  * Input parameter metadata