oakscriptjs 0.10.0 → 0.11.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 (201) hide show
  1. package/dist/cjs/box/index.d.ts.map +1 -1
  2. package/dist/cjs/box/index.js +7 -1
  3. package/dist/cjs/box/index.js.map +2 -2
  4. package/dist/cjs/callsite/index.d.ts +73 -32
  5. package/dist/cjs/callsite/index.d.ts.map +1 -1
  6. package/dist/cjs/callsite/index.js +69 -65
  7. package/dist/cjs/callsite/index.js.map +2 -2
  8. package/dist/cjs/drawing/registry.d.ts +75 -0
  9. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  10. package/dist/cjs/drawing/registry.js +139 -6
  11. package/dist/cjs/drawing/registry.js.map +2 -2
  12. package/dist/cjs/index.d.ts +8 -2
  13. package/dist/cjs/index.d.ts.map +1 -1
  14. package/dist/cjs/index.js +30 -1
  15. package/dist/cjs/index.js.map +2 -2
  16. package/dist/cjs/label/index.d.ts.map +1 -1
  17. package/dist/cjs/label/index.js +3 -0
  18. package/dist/cjs/label/index.js.map +2 -2
  19. package/dist/cjs/line/index.d.ts.map +1 -1
  20. package/dist/cjs/line/index.js +7 -1
  21. package/dist/cjs/line/index.js.map +2 -2
  22. package/dist/cjs/math/index.d.ts.map +1 -1
  23. package/dist/cjs/math/index.js +2 -0
  24. package/dist/cjs/math/index.js.map +2 -2
  25. package/dist/cjs/polyline/index.d.ts.map +1 -1
  26. package/dist/cjs/polyline/index.js +3 -0
  27. package/dist/cjs/polyline/index.js.map +2 -2
  28. package/dist/cjs/runtime/index.d.ts +2 -1
  29. package/dist/cjs/runtime/index.d.ts.map +1 -1
  30. package/dist/cjs/runtime/index.js +2 -0
  31. package/dist/cjs/runtime/index.js.map +2 -2
  32. package/dist/cjs/runtime/input-active.d.ts +27 -0
  33. package/dist/cjs/runtime/input-active.d.ts.map +1 -0
  34. package/dist/cjs/runtime/input-active.js +44 -0
  35. package/dist/cjs/runtime/input-active.js.map +7 -0
  36. package/dist/cjs/runtime/types.d.ts +29 -2
  37. package/dist/cjs/runtime/types.d.ts.map +1 -1
  38. package/dist/cjs/runtime/types.js.map +1 -1
  39. package/dist/cjs/script/index.d.ts +53 -17
  40. package/dist/cjs/script/index.d.ts.map +1 -1
  41. package/dist/cjs/script/index.js +70 -11
  42. package/dist/cjs/script/index.js.map +3 -3
  43. package/dist/cjs/session/bars.d.ts +15 -1
  44. package/dist/cjs/session/bars.d.ts.map +1 -1
  45. package/dist/cjs/session/bars.js +41 -6
  46. package/dist/cjs/session/bars.js.map +3 -3
  47. package/dist/cjs/session/calendar.d.ts +9 -1
  48. package/dist/cjs/session/calendar.d.ts.map +1 -1
  49. package/dist/cjs/session/calendar.js +28 -2
  50. package/dist/cjs/session/calendar.js.map +2 -2
  51. package/dist/cjs/session/context.d.ts +75 -0
  52. package/dist/cjs/session/context.d.ts.map +1 -0
  53. package/dist/cjs/session/context.js +113 -0
  54. package/dist/cjs/session/context.js.map +7 -0
  55. package/dist/cjs/str/index.d.ts +12 -4
  56. package/dist/cjs/str/index.d.ts.map +1 -1
  57. package/dist/cjs/str/index.js +13 -7
  58. package/dist/cjs/str/index.js.map +2 -2
  59. package/dist/cjs/str/numberformat.d.ts +10 -2
  60. package/dist/cjs/str/numberformat.d.ts.map +1 -1
  61. package/dist/cjs/str/numberformat.js +10 -2
  62. package/dist/cjs/str/numberformat.js.map +2 -2
  63. package/dist/cjs/ta/index.d.ts +33 -17
  64. package/dist/cjs/ta/index.d.ts.map +1 -1
  65. package/dist/cjs/ta/index.js +70 -29
  66. package/dist/cjs/ta/index.js.map +2 -2
  67. package/dist/cjs/ta/limits.d.ts +24 -0
  68. package/dist/cjs/ta/limits.d.ts.map +1 -0
  69. package/dist/cjs/ta/limits.js +50 -0
  70. package/dist/cjs/ta/limits.js.map +7 -0
  71. package/dist/cjs/ta/running-sum.d.ts +0 -16
  72. package/dist/cjs/ta/running-sum.d.ts.map +1 -1
  73. package/dist/cjs/ta/running-sum.js +2 -0
  74. package/dist/cjs/ta/running-sum.js.map +2 -2
  75. package/dist/cjs/ta-series.d.ts +7 -7
  76. package/dist/cjs/ta-series.d.ts.map +1 -1
  77. package/dist/cjs/ta-series.js +9 -7
  78. package/dist/cjs/ta-series.js.map +2 -2
  79. package/dist/cjs/time/index.d.ts +5 -2
  80. package/dist/cjs/time/index.d.ts.map +1 -1
  81. package/dist/cjs/time/index.js +2 -2
  82. package/dist/cjs/time/index.js.map +2 -2
  83. package/dist/cjs/time/session.d.ts +17 -1
  84. package/dist/cjs/time/session.d.ts.map +1 -1
  85. package/dist/cjs/time/session.js +61 -7
  86. package/dist/cjs/time/session.js.map +2 -2
  87. package/dist/cjs/types/metadata.d.ts +27 -3
  88. package/dist/cjs/types/metadata.d.ts.map +1 -1
  89. package/dist/cjs/types/metadata.js.map +1 -1
  90. package/dist/esm/box/index.d.ts.map +1 -1
  91. package/dist/esm/box/index.js +8 -2
  92. package/dist/esm/box/index.js.map +1 -1
  93. package/dist/esm/callsite/index.d.ts +73 -32
  94. package/dist/esm/callsite/index.d.ts.map +1 -1
  95. package/dist/esm/callsite/index.js +143 -106
  96. package/dist/esm/callsite/index.js.map +1 -1
  97. package/dist/esm/drawing/registry.d.ts +75 -0
  98. package/dist/esm/drawing/registry.d.ts.map +1 -1
  99. package/dist/esm/drawing/registry.js +237 -6
  100. package/dist/esm/drawing/registry.js.map +1 -1
  101. package/dist/esm/index.d.ts +8 -2
  102. package/dist/esm/index.d.ts.map +1 -1
  103. package/dist/esm/index.js +11 -1
  104. package/dist/esm/index.js.map +1 -1
  105. package/dist/esm/label/index.d.ts.map +1 -1
  106. package/dist/esm/label/index.js +4 -1
  107. package/dist/esm/label/index.js.map +1 -1
  108. package/dist/esm/line/index.d.ts.map +1 -1
  109. package/dist/esm/line/index.js +8 -2
  110. package/dist/esm/line/index.js.map +1 -1
  111. package/dist/esm/math/index.d.ts.map +1 -1
  112. package/dist/esm/math/index.js +3 -0
  113. package/dist/esm/math/index.js.map +1 -1
  114. package/dist/esm/polyline/index.d.ts.map +1 -1
  115. package/dist/esm/polyline/index.js +5 -1
  116. package/dist/esm/polyline/index.js.map +1 -1
  117. package/dist/esm/runtime/index.d.ts +2 -1
  118. package/dist/esm/runtime/index.d.ts.map +1 -1
  119. package/dist/esm/runtime/index.js +1 -0
  120. package/dist/esm/runtime/index.js.map +1 -1
  121. package/dist/esm/runtime/input-active.d.ts +27 -0
  122. package/dist/esm/runtime/input-active.d.ts.map +1 -0
  123. package/dist/esm/runtime/input-active.js +54 -0
  124. package/dist/esm/runtime/input-active.js.map +1 -0
  125. package/dist/esm/runtime/types.d.ts +29 -2
  126. package/dist/esm/runtime/types.d.ts.map +1 -1
  127. package/dist/esm/script/index.d.ts +53 -17
  128. package/dist/esm/script/index.d.ts.map +1 -1
  129. package/dist/esm/script/index.js +80 -13
  130. package/dist/esm/script/index.js.map +1 -1
  131. package/dist/esm/session/bars.d.ts +15 -1
  132. package/dist/esm/session/bars.d.ts.map +1 -1
  133. package/dist/esm/session/bars.js +50 -6
  134. package/dist/esm/session/bars.js.map +1 -1
  135. package/dist/esm/session/calendar.d.ts +9 -1
  136. package/dist/esm/session/calendar.d.ts.map +1 -1
  137. package/dist/esm/session/calendar.js +35 -3
  138. package/dist/esm/session/calendar.js.map +1 -1
  139. package/dist/esm/session/context.d.ts +75 -0
  140. package/dist/esm/session/context.d.ts.map +1 -0
  141. package/dist/esm/session/context.js +122 -0
  142. package/dist/esm/session/context.js.map +1 -0
  143. package/dist/esm/str/index.d.ts +12 -4
  144. package/dist/esm/str/index.d.ts.map +1 -1
  145. package/dist/esm/str/index.js +33 -12
  146. package/dist/esm/str/index.js.map +1 -1
  147. package/dist/esm/str/numberformat.d.ts +10 -2
  148. package/dist/esm/str/numberformat.d.ts.map +1 -1
  149. package/dist/esm/str/numberformat.js +16 -2
  150. package/dist/esm/str/numberformat.js.map +1 -1
  151. package/dist/esm/ta/index.d.ts +33 -17
  152. package/dist/esm/ta/index.d.ts.map +1 -1
  153. package/dist/esm/ta/index.js +130 -56
  154. package/dist/esm/ta/index.js.map +1 -1
  155. package/dist/esm/ta/limits.d.ts +24 -0
  156. package/dist/esm/ta/limits.d.ts.map +1 -0
  157. package/dist/esm/ta/limits.js +42 -0
  158. package/dist/esm/ta/limits.js.map +1 -0
  159. package/dist/esm/ta/running-sum.d.ts +0 -16
  160. package/dist/esm/ta/running-sum.d.ts.map +1 -1
  161. package/dist/esm/ta/running-sum.js +2 -0
  162. package/dist/esm/ta/running-sum.js.map +1 -1
  163. package/dist/esm/ta-series.d.ts +7 -7
  164. package/dist/esm/ta-series.d.ts.map +1 -1
  165. package/dist/esm/ta-series.js +9 -7
  166. package/dist/esm/ta-series.js.map +1 -1
  167. package/dist/esm/time/index.d.ts +5 -2
  168. package/dist/esm/time/index.d.ts.map +1 -1
  169. package/dist/esm/time/index.js +6 -3
  170. package/dist/esm/time/index.js.map +1 -1
  171. package/dist/esm/time/session.d.ts +17 -1
  172. package/dist/esm/time/session.d.ts.map +1 -1
  173. package/dist/esm/time/session.js +97 -10
  174. package/dist/esm/time/session.js.map +1 -1
  175. package/dist/esm/types/metadata.d.ts +27 -3
  176. package/dist/esm/types/metadata.d.ts.map +1 -1
  177. package/package.json +1 -1
  178. package/src/box/index.ts +8 -2
  179. package/src/callsite/index.ts +150 -100
  180. package/src/drawing/registry.ts +255 -5
  181. package/src/index.ts +36 -1
  182. package/src/label/index.ts +4 -1
  183. package/src/line/index.ts +8 -2
  184. package/src/math/index.ts +2 -0
  185. package/src/polyline/index.ts +3 -1
  186. package/src/runtime/index.ts +3 -0
  187. package/src/runtime/input-active.ts +50 -0
  188. package/src/runtime/types.ts +25 -2
  189. package/src/script/index.ts +123 -23
  190. package/src/session/bars.ts +47 -6
  191. package/src/session/calendar.ts +37 -3
  192. package/src/session/context.ts +160 -0
  193. package/src/str/index.ts +30 -12
  194. package/src/str/numberformat.ts +18 -3
  195. package/src/ta/index.ts +148 -66
  196. package/src/ta/limits.ts +47 -0
  197. package/src/ta/running-sum.ts +2 -0
  198. package/src/ta-series.ts +16 -14
  199. package/src/time/index.ts +6 -3
  200. package/src/time/session.ts +89 -9
  201. package/src/types/metadata.ts +27 -3
@@ -69,6 +69,7 @@ import type {
69
69
  FillConfig,
70
70
  HLineConfig,
71
71
  InputConfig,
72
+ InputCondition,
72
73
  PlotConfig,
73
74
  ShapeConfig,
74
75
  } from '../runtime/types.js';
@@ -86,12 +87,14 @@ import * as tableCore from '../table/index.js';
86
87
  import * as chartPointCore from '../chartpoint/index.js';
87
88
  import * as textCore from '../text/index.js';
88
89
  import * as mathCore from '../math/index.js';
89
- import { resetDrawings, setDrawingLimits } from '../drawing/registry.js';
90
+ import { resetDrawings, setDrawingBar, setDrawingLimits } from '../drawing/registry.js';
91
+ export { holdDrawing, releaseDrawing, varDrawing, type DrawingVariable } from '../drawing/registry.js';
90
92
  import { drawingOutputs } from '../drawing/output.js';
91
93
  import * as timeframeCore from '../timeframe/index.js';
92
94
  import * as timeCore from '../time/index.js';
93
95
  import * as strCore from '../str/index.js';
94
96
  import { formatMessage } from '../str/messageformat.js';
97
+ import { tickScale } from '../str/numberformat.js';
95
98
  import { TradingCalendar, type SessionSpec } from '../session/calendar.js';
96
99
  import { SessionBars, sessionFlags, type SessionFlags } from '../session/bars.js';
97
100
  import { afterCompletion, heikinAshi, mapToChart, periodsOf, resample } from '../security/resample.js';
@@ -294,6 +297,7 @@ export function executeScript(
294
297
  ): ScriptRunResult {
295
298
  ctxBars.setAll(barsInput);
296
299
  chartCtx = { ...chart };
300
+ runNow = chart.now ?? Date.now();
297
301
  scriptOptions = options;
298
302
  ctx = freshCollector(inputs);
299
303
  resetDrawings();
@@ -380,13 +384,19 @@ function declare(c: Collector, kind: 'indicator' | 'strategy'): void {
380
384
  c.declaration = kind;
381
385
  }
382
386
 
383
- /** Options of every `input.*` kind (PineScript `group`, `inline`, `tooltip`, `confirm`, `display`). */
387
+ /** Options of every `input.*` kind (PineScript `group`, `inline`, `tooltip`, `confirm`, `display`, `active`). */
384
388
  export interface InputOptions {
385
389
  group?: string;
386
390
  inline?: string;
387
391
  tooltip?: string;
388
392
  confirm?: boolean;
389
393
  display?: InputConfig['display'];
394
+ /**
395
+ * PineScript `active`: false greys the input out in the settings (it keeps its value). A boolean as in
396
+ * PineScript (`active: showMa`), or a condition on input ids that a host can apply without running the script
397
+ * (`active: 'ma_1'`, `active: { input: 'type', ne: 'None' }`)
398
+ */
399
+ active?: boolean | InputCondition;
390
400
  }
391
401
 
392
402
  export interface NumericInputOptions extends InputOptions {
@@ -407,6 +417,7 @@ function common(opts: InputOptions): Partial<InputConfig> {
407
417
  if (opts.tooltip !== undefined) out.tooltip = opts.tooltip;
408
418
  if (opts.confirm !== undefined) out.confirm = opts.confirm;
409
419
  if (opts.display !== undefined) out.display = opts.display;
420
+ if (opts.active !== undefined) out.active = opts.active;
410
421
  return out;
411
422
  }
412
423
 
@@ -538,25 +549,27 @@ export const ta = {
538
549
  ): [Series, Series, Series, Series, Series] =>
539
550
  taSeries.ichimoku(bars(), conversionPeriods, basePeriods, laggingSpan2Periods, displacement),
540
551
  /** `ta.highest(source, length)` or `ta.highest(length)` (source = high). */
541
- highest: (sourceOrLength: Series | number, length?: number): Series =>
552
+ highest: (sourceOrLength: Series | number, length?: number | Series): Series =>
542
553
  typeof sourceOrLength === 'number' ? taSeries.highest(high, sourceOrLength) : taSeries.highest(sourceOrLength, length!),
543
554
  /** `ta.lowest(source, length)` or `ta.lowest(length)` (source = low). */
544
- lowest: (sourceOrLength: Series | number, length?: number): Series =>
555
+ lowest: (sourceOrLength: Series | number, length?: number | Series): Series =>
545
556
  typeof sourceOrLength === 'number' ? taSeries.lowest(low, sourceOrLength) : taSeries.lowest(sourceOrLength, length!),
546
557
  /** `ta.highestbars(source, length)` or `ta.highestbars(length)` (source = high); offsets are 0 or negative. */
547
- highestbars: (sourceOrLength: Series | number, length?: number): Series =>
558
+ highestbars: (sourceOrLength: Series | number, length?: number | Series): Series =>
548
559
  typeof sourceOrLength === 'number'
549
560
  ? taSeries.highestbars(high, sourceOrLength)
550
561
  : taSeries.highestbars(sourceOrLength, length!),
551
562
  /** `ta.lowestbars(source, length)` or `ta.lowestbars(length)` (source = low); offsets are 0 or negative. */
552
- lowestbars: (sourceOrLength: Series | number, length?: number): Series =>
563
+ lowestbars: (sourceOrLength: Series | number, length?: number | Series): Series =>
553
564
  typeof sourceOrLength === 'number'
554
565
  ? taSeries.lowestbars(low, sourceOrLength)
555
566
  : taSeries.lowestbars(sourceOrLength, length!),
556
567
  /** `ta.vwap(source, anchor?, stdev_mult?)` with the chart volume; the default anchor is a new trading
557
- * day (`timeframe.change("1D")`), which needs the chart context (timeframe, time zone, session). */
568
+ * day (`timeframe.change("1D")`), which needs the chart context (timeframe, time zone, session), and the
569
+ * first bar: as in PineScript, the first loaded bars (a day in progress) have a value. */
558
570
  vwap: ((source: Series, anchor?: Series, stdev_mult?: number): Series | [Series, Series, Series] => {
559
- const a = anchor ?? changeOf('1D'); // PineScript default: timeframe.change("1D")
571
+ // PineScript default: timeframe.change("1D"); the sums also start on the first bar (a day in progress)
572
+ const a = anchor ?? changeOf('1D', true);
560
573
  return stdev_mult === undefined ? taSeries.vwap(source, volume, a) : taSeries.vwapBands(source, volume, a, stdev_mult);
561
574
  }) as {
562
575
  (source: Series, anchor?: Series): Series;
@@ -566,12 +579,12 @@ export const ta = {
566
579
  mfi: (source: Series, length: number): Series => taSeries.mfi(source, length, volume),
567
580
  /** `ta.pivothigh(source, leftbars, rightbars)` or `ta.pivothigh(leftbars, rightbars)` (source = high).
568
581
  * The value appears `rightbars` bars after the pivot bar, as in PineScript. */
569
- pivothigh: (sourceOrLeft: Series | number, leftOrRight: number, rightbars?: number): Series =>
582
+ pivothigh: (sourceOrLeft: Series | number, leftOrRight: number | Series, rightbars?: number | Series): Series =>
570
583
  typeof sourceOrLeft === 'number'
571
584
  ? taSeries.pivothigh(high, sourceOrLeft, leftOrRight)
572
585
  : taSeries.pivothigh(sourceOrLeft, leftOrRight, rightbars!),
573
586
  /** `ta.pivotlow(source, leftbars, rightbars)` or `ta.pivotlow(leftbars, rightbars)` (source = low). */
574
- pivotlow: (sourceOrLeft: Series | number, leftOrRight: number, rightbars?: number): Series =>
587
+ pivotlow: (sourceOrLeft: Series | number, leftOrRight: number | Series, rightbars?: number | Series): Series =>
575
588
  typeof sourceOrLeft === 'number'
576
589
  ? taSeries.pivotlow(low, sourceOrLeft, leftOrRight)
577
590
  : taSeries.pivotlow(sourceOrLeft, leftOrRight, rightbars!),
@@ -1253,6 +1266,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1253
1266
  for (idx = 0; idx < b.length; idx++) {
1254
1267
  if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
1255
1268
  currentBar = idx;
1269
+ setDrawingBar(idx, idx === b.length - 1);
1256
1270
  const r = fn(ctx);
1257
1271
  out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
1258
1272
  // grow the history buffers from the largest reference of this bar
@@ -1269,6 +1283,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1269
1283
  }
1270
1284
  } finally {
1271
1285
  currentBar = -1;
1286
+ setDrawingBar(null);
1272
1287
  }
1273
1288
  return Series.fromArray(ctxBars, out);
1274
1289
  }
@@ -1522,12 +1537,32 @@ export interface ChartContext {
1522
1537
  tickerid?: string;
1523
1538
  /** Unit of `Bar.time`: 's' (default, as lightweight-charts) or 'ms'. PineScript times are in ms. */
1524
1539
  timeUnit?: 's' | 'ms';
1525
- /** Minimum price move of the symbol (PineScript `syminfo.mintick`), e.g. 0.01; used by `math.round_to_mintick` */
1540
+ /**
1541
+ * Minimum price move of the symbol (PineScript `syminfo.mintick`), e.g. 0.01; used by `math.round_to_mintick` and
1542
+ * `format.mintick`. Default: `minmove / pricescale` when both are given
1543
+ */
1526
1544
  mintick?: number;
1545
+ /**
1546
+ * PineScript `syminfo.minmove` and `syminfo.pricescale` (mintick = minmove / pricescale). Default: from `mintick`,
1547
+ * with pricescale 10^(its decimals) (0.25 is 25 / 100). Pass them for a symbol priced in fractions (1 / 32):
1548
+ * `format.mintick` shows floor(log10(pricescale)) decimals
1549
+ */
1550
+ minmove?: number;
1551
+ pricescale?: number;
1527
1552
  /** Currency value of one point of price move for one contract (PineScript `syminfo.pointvalue`); 1 for stocks */
1528
1553
  pointvalue?: number;
1529
1554
  /** Smallest tradable quantity (PineScript `syminfo.mincontract`); 1 for whole shares */
1530
1555
  mincontract?: number;
1556
+ /** Symbol type (PineScript `syminfo.type`): "stock", "crypto", "forex", "futures", "index", "fund"... */
1557
+ symbolType?: string;
1558
+ /** Currency of the symbol (PineScript `syminfo.currency`), e.g. "USD" */
1559
+ currency?: string;
1560
+ /** Chart foreground colour (PineScript `chart.fg_color`), e.g. "#DBDBDB" on a dark chart */
1561
+ foregroundColor?: string;
1562
+ /** Chart background colour (PineScript `chart.bg_color`), e.g. "#0F0F0F" on a dark chart */
1563
+ backgroundColor?: string;
1564
+ /** Current time in ms (PineScript `timenow`); default: the clock of the computer at the start of the run */
1565
+ now?: number;
1531
1566
  /**
1532
1567
  * The last bar is closed (`barstate.isconfirmed` on the last bar). Default false: PineScript treats the last bar
1533
1568
  * of the data as not confirmed until the data feed closes it.
@@ -1611,19 +1646,48 @@ function symbolProperty(key: 'mintick' | 'pointvalue' | 'mincontract'): number {
1611
1646
  collector();
1612
1647
  const value = chartCtx[key];
1613
1648
  if (value === undefined) {
1649
+ if (key === 'mintick' && chartCtx.minmove !== undefined && chartCtx.pricescale !== undefined) {
1650
+ return chartCtx.minmove / chartCtx.pricescale;
1651
+ }
1614
1652
  throw new Error(`syminfo.${key} is not known: pass it as executeScript(body, bars, inputs, { ${key} }).`);
1615
1653
  }
1616
1654
  return value;
1617
1655
  }
1618
1656
 
1657
+ /** A text value of the chart or the symbol that the host passes in the chart context. */
1658
+ function chartText(key: 'symbolType' | 'currency' | 'foregroundColor' | 'backgroundColor', name: string): string {
1659
+ collector();
1660
+ const value = chartCtx[key];
1661
+ if (value === undefined) {
1662
+ throw new Error(`${name} is not known: pass it as executeScript(body, bars, inputs, { ${key} }).`);
1663
+ }
1664
+ return value;
1665
+ }
1666
+
1667
+ /** `[minmove, pricescale]` of the chart symbol: from the chart context, else from its mintick. */
1668
+ function symbolScale(): [number, number] {
1669
+ collector();
1670
+ const { minmove, pricescale } = chartCtx;
1671
+ if (minmove !== undefined && pricescale !== undefined) return [minmove, pricescale];
1672
+ const [move, scale] = tickScale(symbolProperty('mintick'), pricescale);
1673
+ return [minmove ?? move, scale];
1674
+ }
1675
+
1619
1676
  /**
1620
1677
  * PineScript `syminfo.timezone`, `syminfo.tickerid`, `syminfo.session` (session type), `syminfo.mintick`,
1621
- * `syminfo.pointvalue` and `syminfo.mincontract`, from the chart context.
1678
+ * `syminfo.minmove`, `syminfo.pricescale`, `syminfo.pointvalue`, `syminfo.mincontract`, `syminfo.type` and
1679
+ * `syminfo.currency`, from the chart context.
1622
1680
  */
1623
1681
  export const syminfo = {
1624
1682
  get mintick(): number {
1625
1683
  return symbolProperty('mintick');
1626
1684
  },
1685
+ get minmove(): number {
1686
+ return symbolScale()[0];
1687
+ },
1688
+ get pricescale(): number {
1689
+ return symbolScale()[1];
1690
+ },
1627
1691
  get pointvalue(): number {
1628
1692
  return symbolProperty('pointvalue');
1629
1693
  },
@@ -1644,6 +1708,12 @@ export const syminfo = {
1644
1708
  collector();
1645
1709
  return chartCtx.sessionType ?? 'regular';
1646
1710
  },
1711
+ get type(): string {
1712
+ return chartText('symbolType', 'syminfo.type');
1713
+ },
1714
+ get currency(): string {
1715
+ return chartText('currency', 'syminfo.currency');
1716
+ },
1647
1717
  };
1648
1718
 
1649
1719
  /** A calendar function: `(time, timezone?)` on a UNIX time or on a Series of times. */
@@ -1709,9 +1779,11 @@ export function inSession(time: number, session: string, timezone?: string): boo
1709
1779
  /** PineScript `str.*`; `format_time` and the dates of `format` use the exchange time zone by default. */
1710
1780
  export const str = {
1711
1781
  ...strCore,
1712
- // format.mintick rounds to the chart symbol's tick (syminfo.mintick)
1713
- tostring: ((value: unknown, format?: string, mintick?: number) =>
1714
- strCore.tostring(value as never, format, format === 'mintick' ? (mintick ?? syminfo.mintick) : mintick)) as typeof strCore.tostring,
1782
+ // format.mintick uses the chart symbol's tick and price scale (syminfo.mintick, syminfo.pricescale)
1783
+ tostring: ((value: unknown, format?: string, mintick?: number, pricescale?: number) =>
1784
+ format === 'mintick' && mintick === undefined
1785
+ ? strCore.tostring(value as never, format, syminfo.mintick, pricescale ?? syminfo.pricescale)
1786
+ : strCore.tostring(value as never, format, mintick, pricescale)) as typeof strCore.tostring,
1715
1787
  format_time(time: number, format: string = "yyyy-MM-dd'T'HH:mm:ssZ", timezone?: string): string {
1716
1788
  return strCore.format_time(time, format, timezone ?? exchangeTimezone());
1717
1789
  },
@@ -1755,7 +1827,10 @@ function sessionBars(): { bars: SessionBars; regular: SessionBars } {
1755
1827
  * session. As in PineScript, an explicit time zone reads the symbol session hours in that zone without
1756
1828
  * corrections and holidays, and an explicit session follows PineScript's session string rules.
1757
1829
  */
1758
- function calendarFor(session?: string, timezone?: string): { bars: SessionBars; test: boolean } {
1830
+ function calendarFor(
1831
+ session?: string,
1832
+ timezone?: string
1833
+ ): { bars: SessionBars; test: boolean; inside?: (time: number) => boolean } {
1759
1834
  const { bars } = sessionBars();
1760
1835
  if (!session && !timezone) return { bars, test: false };
1761
1836
  const tz = timezone ?? exchangeTimezone();
@@ -1767,7 +1842,14 @@ function calendarFor(session?: string, timezone?: string): { bars: SessionBars;
1767
1842
  return { bars: new SessionBars(new TradingCalendar(tz, { session: toSpec(symbol).session }, 'symbol'), bars.chart.period), test: true };
1768
1843
  }
1769
1844
  const spec = session ? { session } : { session: toSpec(chartCtx.session!).session };
1770
- return { bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol'), bars.chart.period), test: true };
1845
+ return {
1846
+ bars: new SessionBars(new TradingCalendar(tz, spec, session ? 'pine' : 'symbol'), bars.chart.period),
1847
+ test: true,
1848
+ // a PineScript session string: the rules of time.inSession (runs ending at 0000, days around a clock change)
1849
+ inside: session
1850
+ ? (time) => timeCore.inSession(time, session, tz, timeframeCore.in_seconds(bars.chart.period) * 1000)
1851
+ : undefined,
1852
+ };
1771
1853
  }
1772
1854
 
1773
1855
  function perBar(fn: (t: number, i: number, times: number[]) => number): Series {
@@ -1784,22 +1866,28 @@ function callableSeries<F extends (...args: never[]) => Series>(series: Series,
1784
1866
 
1785
1867
  function timeFunction(tf?: string, session?: string, timezone?: string): Series {
1786
1868
  collector();
1787
- const { bars, test } = calendarFor(session, timezone);
1869
+ const { bars, test, inside } = calendarFor(session, timezone);
1788
1870
  const timeframeString = tf || chartTimeframe().period;
1789
- return perBar((t) => (test && !bars.inside(t) ? NaN : bars.timeOf(t, timeframeString)));
1871
+ return perBar((t) => (test && !(inside ? inside(t) : bars.inside(t)) ? NaN : bars.timeOf(t, timeframeString)));
1790
1872
  }
1791
1873
 
1792
1874
  function timeCloseFunction(tf?: string, session?: string, timezone?: string): Series {
1793
1875
  collector();
1794
- const { bars, test } = calendarFor(session, timezone);
1876
+ const { bars, test, inside } = calendarFor(session, timezone);
1795
1877
  const timeframeString = tf || chartTimeframe().period;
1796
- return perBar((t) => (test && !bars.inside(t) ? NaN : bars.closeOf(t, timeframeString)));
1878
+ return perBar((t) => (test && !(inside ? inside(t) : bars.inside(t)) ? NaN : bars.closeOf(t, timeframeString)));
1797
1879
  }
1798
1880
 
1799
1881
  /**
1800
1882
  * PineScript `time`: the bar open time in ms (a Series), and `time(timeframe?, session?, timezone?)`:
1801
1883
  * the open time of the `timeframe` period that contains the bar, `na` outside `session`.
1802
1884
  */
1885
+ /** Current time of the run in ms, see `ChartContext.now`. */
1886
+ let runNow = NaN;
1887
+
1888
+ /** PineScript `timenow`: the current time in ms, the same on every bar of a run (`ChartContext.now`). */
1889
+ export const timenow = new Series(ctxBars, () => runNow);
1890
+
1803
1891
  export const time = callableSeries(new Series(ctxBars, (b) => barTime(b)), timeFunction);
1804
1892
 
1805
1893
  /**
@@ -1814,9 +1902,11 @@ export const time_close = callableSeries(
1814
1902
  /** PineScript `time_tradingday`: 00:00 UTC of the trading day of each bar, in ms. */
1815
1903
  export const time_tradingday = new Series(ctxBars, (b) => sessionBars().bars.tradingDayTime(barTime(b)));
1816
1904
 
1817
- function changeOf(tf: string): Series {
1905
+ function changeOf(tf: string, firstBar = false): Series {
1818
1906
  const { bars } = sessionBars();
1819
- return perBar((t, i, times) => (i > 0 && bars.timeOf(t, tf) !== bars.timeOf(times[i - 1]!, tf) ? 1 : 0));
1907
+ // a bar without a period (after an early close) is no change, and neither is the bar after it
1908
+ const changed = (a: number, b: number): boolean => !Number.isNaN(a) && !Number.isNaN(b) && a !== b;
1909
+ return perBar((t, i, times) => ((i > 0 ? changed(bars.timeOf(t, tf), bars.timeOf(times[i - 1]!, tf)) : firstBar) ? 1 : 0));
1820
1910
  }
1821
1911
 
1822
1912
  function flag(key: keyof SessionFlags): Series {
@@ -2052,6 +2142,14 @@ export const text = { ...textCore };
2052
2142
  * (index, time, and `price`, default the close), so it runs inside eachBar().
2053
2143
  */
2054
2144
  export const chart = {
2145
+ /** Chart foreground colour (PineScript `chart.fg_color`), from the chart context. */
2146
+ get fg_color(): string {
2147
+ return chartText('foregroundColor', 'chart.fg_color');
2148
+ },
2149
+ /** Chart background colour (PineScript `chart.bg_color`), from the chart context. */
2150
+ get bg_color(): string {
2151
+ return chartText('backgroundColor', 'chart.bg_color');
2152
+ },
2055
2153
  point: {
2056
2154
  ...chartPointCore,
2057
2155
  now(price?: number): ChartPoint {
@@ -2115,11 +2213,13 @@ export const math = {
2115
2213
  }) as typeof mathCore.round_to_mintick,
2116
2214
  };
2117
2215
  export * as compare from '../compare/index.js';
2216
+ export { isInputActive } from '../runtime/input-active.js';
2118
2217
  export type {
2119
2218
  Bar,
2120
2219
  IndicatorResult,
2121
2220
  TimeValue,
2122
2221
  InputConfig,
2222
+ InputCondition,
2123
2223
  PlotConfig,
2124
2224
  HLineConfig,
2125
2225
  FillConfig,
@@ -111,7 +111,10 @@ export class SessionBars {
111
111
  return p[p.length - 1]![1];
112
112
  }
113
113
 
114
- /** PineScript `time(tf)` for a bar that opens at `time`; NaN when no trading day is found. */
114
+ /**
115
+ * PineScript `time(tf)` for a bar that opens at `time`; NaN when no trading day is found. An intraday bar that
116
+ * opens after an early close has no day: `time("D")` is NaN on it (measured; `time("W")` keeps its value).
117
+ */
115
118
  timeOf(time: number, timeframe: string): number {
116
119
  const tf = info(timeframe);
117
120
  const at = this.calendar.locate(time);
@@ -123,6 +126,7 @@ export class SessionBars {
123
126
  return start + Math.floor((time - start) / ms) * ms;
124
127
  }
125
128
  const { unit, n } = unitOf(tf);
129
+ if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
126
130
  const [from, to] = this.periodRange(at.date, unit, n);
127
131
  const first = this.calendar.firstTradingDay(from, to);
128
132
  return first === null ? NaN : this.dayStart(first);
@@ -140,6 +144,7 @@ export class SessionBars {
140
144
  return Math.min(this.timeOf(time, timeframe) + in_seconds(tf.period) * 1000, end);
141
145
  }
142
146
  const { unit, n } = unitOf(tf);
147
+ if (unit === 'D' && n === 1 && this.stray(time)) return NaN;
143
148
  const [from, to] = this.periodRange(at.date, unit, n);
144
149
  // intraday charts: the start of the next period, except "1D" (the session end)
145
150
  if (this.chart.isintraday && (unit !== 'D' || n > 1)) {
@@ -154,7 +159,7 @@ export class SessionBars {
154
159
  /** Trading day of a bar: on D / W / M charts the last trading day of the chart period. */
155
160
  tradingDay(time: number): DateNum | null {
156
161
  const at = this.calendar.locate(time);
157
- if (!at) return null;
162
+ if (!at || this.stray(time)) return null;
158
163
  if (this.chart.isdwm) {
159
164
  const { unit, n } = unitOf(this.chart);
160
165
  const [from, to] = this.periodRange(at.date, unit, n);
@@ -188,6 +193,35 @@ export class SessionBars {
188
193
  return day === null ? NaN : dateUtcMs(day);
189
194
  }
190
195
 
196
+ /**
197
+ * Open time of the bar after the bar that opens at `time`, on the trading calendar: the next bar of the session
198
+ * period, else the start of the next period or trading day; on D / W / M charts the session start of the first
199
+ * trading day of the next chart period. NaN when none is found within 400 days.
200
+ */
201
+ nextBarTime(time: number): number {
202
+ const at = this.calendar.locate(time);
203
+ if (!at) return NaN;
204
+ if (!this.chart.isdwm) {
205
+ const periods = this.calendar.periods(at.date);
206
+ const [start, end] = periods[at.index]!;
207
+ const ms = in_seconds(this.chart.period) * 1000;
208
+ const next = start + (Math.floor((time - start) / ms) + 1) * ms;
209
+ if (at.inside && next < end) return next;
210
+ if (at.index + 1 < periods.length) return periods[at.index + 1]![0];
211
+ const day = this.calendar.firstTradingDay(addDays(at.date, 1), addDays(at.date, 400));
212
+ return day === null ? NaN : this.dayStart(day);
213
+ }
214
+ const { unit, n } = unitOf(this.chart);
215
+ const [, to] = this.periodRange(at.date, unit, n);
216
+ const day = this.calendar.firstTradingDay(addDays(to, 1), addDays(to, 400));
217
+ return day === null ? NaN : this.dayStart(day);
218
+ }
219
+
220
+ /** An intraday bar that opens after an early close of its day (see TradingCalendar.afterEarlyClose). */
221
+ stray(time: number): boolean {
222
+ return !this.chart.isdwm && this.calendar.afterEarlyClose(time);
223
+ }
224
+
191
225
  /** True when the bar at `time` is inside the session. */
192
226
  inside(time: number): boolean {
193
227
  return this.calendar.locate(time)?.inside ?? false;
@@ -217,6 +251,9 @@ export interface SessionFlags {
217
251
  * time; first and last bars are per session period (a trading day can have two periods, e.g. after a
218
252
  * futures holiday); pre / post market compare with the regular hours of the same trading day; on
219
253
  * D / W / M charts every bar is its own session (first, last, market, first and last regular).
254
+ * The first loaded bar is a first bar only when no earlier bar of its period can exist (it opens less than one chart
255
+ * period after the period start). A bar that opens after an early close is in the market hours (the usual hours of
256
+ * its day) but is no last bar.
220
257
  */
221
258
  export function sessionFlags(times: number[], bars: SessionBars, regular: SessionBars): SessionFlags {
222
259
  const n = times.length;
@@ -242,18 +279,22 @@ export function sessionFlags(times: number[], bars: SessionBars, regular: Sessio
242
279
  const close = bars.closeOf(t, chart.period);
243
280
  const [, end] = bars.calendar.periods(at.date)[at.index]!;
244
281
  const period = `${at.date}/${at.index}`;
245
- out.isfirstbar[i] = period !== previousPeriod;
246
- out.islastbar[i] = close >= end;
282
+ const [start] = bars.calendar.periods(at.date)[at.index]!;
283
+ const barMs = in_seconds(chart.period) * 1000;
284
+ // the first loaded bar: a first bar only when no earlier bar of the period can exist
285
+ out.isfirstbar[i] = i === 0 ? t - start < barMs : period !== previousPeriod;
286
+ const stray = bars.stray(t);
287
+ out.islastbar[i] = !stray && close >= end;
247
288
  const regularPeriods = regular.calendar.periods(at.date);
248
289
  const r = regularPeriods.findIndex(([s, e]) => s <= t && t < e);
249
290
  const market = r !== -1;
250
- out.ismarket[i] = market;
291
+ out.ismarket[i] = market || regular.stray(t);
251
292
  if (!market && regularPeriods.length) {
252
293
  out.ispremarket[i] = t < regularPeriods[0]![0];
253
294
  out.ispostmarket[i] = t >= regularPeriods[regularPeriods.length - 1]![1];
254
295
  }
255
296
  const regularPeriod = market ? `${at.date}/${r}` : '';
256
- out.isfirstbar_regular[i] = market && regularPeriod !== previousRegular;
297
+ out.isfirstbar_regular[i] = market && (i === 0 ? t - regularPeriods[r]![0] < barMs : regularPeriod !== previousRegular);
257
298
  out.islastbar_regular[i] = market && close >= regularPeriods[r]![1];
258
299
  previousPeriod = period;
259
300
  previousRegular = regularPeriod;
@@ -6,7 +6,10 @@
6
6
  * - periods "HHMM-HHMM", several separated by commas; "24x7" is the whole day, every day
7
7
  * - a period whose end is not after its start ("1700-1600", "1700-1700") starts the day before the
8
8
  * trading day: the session that opens Sunday 17:00 is Monday's
9
- * - "HHMMF<n>" moves a time <n> days before the trading day ("1700F2-1200F1")
9
+ * - "HHMMF<n>" moves a time <n> days before the trading day ("1700F2-1200F1"); "F" alone is one day ("1700F-1900":
10
+ * 17:00 of the day before to 19:00)
11
+ * - several sections joined by "|", each with its own days ("1900-1900:3456|1700F-1900:2": Monday opens on Sunday
12
+ * 17:00, the other days at 19:00 of the day before)
10
13
  * - optional days after ":" (1 = Sunday ... 7 = Saturday); without days a symbol session trades Monday to
11
14
  * Friday ("24x7": every day), and a PineScript session string follows PineScript's rules (one period:
12
15
  * every day; several periods: Monday to Friday)
@@ -62,9 +65,10 @@ export const weekdayOf = (date: DateNum): number => new Date(utcOf(date)).getUTC
62
65
  export const dateUtcMs = utcOf;
63
66
 
64
67
  function parseTime(token: string, spec: string): { minutes: number; offset?: number } {
65
- const m = /^(\d{2})(\d{2})(?:F(\d+))?$/.exec(token.trim());
68
+ const m = /^(\d{2})(\d{2})(?:F(\d*))?$/.exec(token.trim());
66
69
  if (!m) throw new SyntaxError(`Invalid session "${spec}"`);
67
- return { minutes: Number(m[1]) * 60 + Number(m[2]), offset: m[3] === undefined ? undefined : Number(m[3]) };
70
+ // "F" without a number is one day before the trading day
71
+ return { minutes: Number(m[1]) * 60 + Number(m[2]), offset: m[3] === undefined ? undefined : Number(m[3] || 1) };
68
72
  }
69
73
 
70
74
  function parsePeriods(text: string, spec: string): Period[] {
@@ -102,6 +106,17 @@ function pineVersion(text: string, from: DateNum): Version {
102
106
 
103
107
  function parseVersion(text: string, from: DateNum, spec: string, rule: 'symbol' | 'pine'): Version {
104
108
  if (rule === 'pine') return pineVersion(text, from);
109
+ if (text.includes('|')) {
110
+ // sections with their own days
111
+ const byDay: Period[][] = Array.from({ length: 8 }, () => []);
112
+ for (const section of text.split('|').filter((part) => part.trim())) {
113
+ const one = parseVersion(section, from, spec, rule);
114
+ one.days.forEach((on, d) => {
115
+ if (on) byDay[d] = one.periods;
116
+ });
117
+ }
118
+ return { from, periods: [], days: byDay.map((p) => p.length > 0), byDay };
119
+ }
105
120
  const [periodText = '', dayText] = text.trim().split(':');
106
121
  const periods = parsePeriods(periodText, spec);
107
122
  const allDays = periodText.trim() === '24x7';
@@ -174,6 +189,25 @@ export class TradingCalendar {
174
189
  return this.periods(date).length > 0;
175
190
  }
176
191
 
192
+ /**
193
+ * True for a bar that opens after the session of its day was cut by a correction (an early close) but inside the
194
+ * usual hours of that day: PineScript has no period for such a bar (`time("D")` is na on it).
195
+ */
196
+ afterEarlyClose(time: number): boolean {
197
+ const at = this.locate(time);
198
+ if (!at || at.inside || !this.corrections.get(at.date)) return false;
199
+ const version = [...this.versions].reverse().find((v) => v.from <= at.date)!;
200
+ const usual = version.byDay ? version.byDay[weekdayOf(at.date)]! : version.days[weekdayOf(at.date)] ? version.periods : [];
201
+ const y = Math.floor(at.date / 10000);
202
+ const m = Math.floor(at.date / 100) % 100;
203
+ const d = at.date % 100;
204
+ return usual.some(
205
+ (p) =>
206
+ zonedToUnix(this.timezone, y, m, d - p.startOffset, Math.floor(p.start / 60), p.start % 60) <= time &&
207
+ time < zonedToUnix(this.timezone, y, m, d - p.endOffset, Math.floor(p.end / 60), p.end % 60)
208
+ );
209
+ }
210
+
177
211
  /**
178
212
  * Trading day and period of a time: the period that contains it, else the last period that started
179
213
  * before it. Null when no trading day is found near the time.