oakscriptjs 0.12.2 → 0.13.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 (49) hide show
  1. package/dist/cjs/drawing/registry.d.ts +37 -0
  2. package/dist/cjs/drawing/registry.d.ts.map +1 -1
  3. package/dist/cjs/drawing/registry.js +40 -0
  4. package/dist/cjs/drawing/registry.js.map +2 -2
  5. package/dist/cjs/index.d.ts +1 -1
  6. package/dist/cjs/index.js +1 -1
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/math/index.d.ts +4 -0
  9. package/dist/cjs/math/index.d.ts.map +1 -1
  10. package/dist/cjs/math/index.js +9 -0
  11. package/dist/cjs/math/index.js.map +2 -2
  12. package/dist/cjs/runtime/types.d.ts +6 -0
  13. package/dist/cjs/runtime/types.d.ts.map +1 -1
  14. package/dist/cjs/runtime/types.js.map +1 -1
  15. package/dist/cjs/script/index.d.ts +52 -1
  16. package/dist/cjs/script/index.d.ts.map +1 -1
  17. package/dist/cjs/script/index.js +217 -11
  18. package/dist/cjs/script/index.js.map +3 -3
  19. package/dist/cjs/strategy/index.d.ts +3 -0
  20. package/dist/cjs/strategy/index.d.ts.map +1 -1
  21. package/dist/cjs/strategy/index.js +1 -0
  22. package/dist/cjs/strategy/index.js.map +2 -2
  23. package/dist/esm/drawing/registry.d.ts +37 -0
  24. package/dist/esm/drawing/registry.d.ts.map +1 -1
  25. package/dist/esm/drawing/registry.js +41 -0
  26. package/dist/esm/drawing/registry.js.map +1 -1
  27. package/dist/esm/index.d.ts +1 -1
  28. package/dist/esm/index.js +1 -1
  29. package/dist/esm/math/index.d.ts +4 -0
  30. package/dist/esm/math/index.d.ts.map +1 -1
  31. package/dist/esm/math/index.js +10 -0
  32. package/dist/esm/math/index.js.map +1 -1
  33. package/dist/esm/runtime/types.d.ts +6 -0
  34. package/dist/esm/runtime/types.d.ts.map +1 -1
  35. package/dist/esm/script/index.d.ts +52 -1
  36. package/dist/esm/script/index.d.ts.map +1 -1
  37. package/dist/esm/script/index.js +254 -10
  38. package/dist/esm/script/index.js.map +1 -1
  39. package/dist/esm/strategy/index.d.ts +3 -0
  40. package/dist/esm/strategy/index.d.ts.map +1 -1
  41. package/dist/esm/strategy/index.js +1 -0
  42. package/dist/esm/strategy/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/drawing/registry.ts +56 -0
  45. package/src/index.ts +1 -1
  46. package/src/math/index.ts +11 -0
  47. package/src/runtime/types.ts +6 -0
  48. package/src/script/index.ts +338 -10
  49. package/src/strategy/index.ts +4 -0
@@ -88,7 +88,15 @@ import * as tableCore from '../table/index.js';
88
88
  import * as chartPointCore from '../chartpoint/index.js';
89
89
  import * as textCore from '../text/index.js';
90
90
  import * as mathCore from '../math/index.js';
91
- import { createdDrawingKinds, resetDrawings, setDrawingBar, setDrawingLimits, type DrawingKind } from '../drawing/registry.js';
91
+ import {
92
+ createdDrawingKinds,
93
+ resetDrawings,
94
+ restoreDrawingState,
95
+ saveDrawingState,
96
+ setDrawingBar,
97
+ setDrawingLimits,
98
+ type DrawingKind,
99
+ } from '../drawing/registry.js';
92
100
  export { holdDrawing, releaseDrawing, varDrawing, type DrawingVariable } from '../drawing/registry.js';
93
101
  import { drawingOutputs } from '../drawing/output.js';
94
102
  import * as timeframeCore from '../timeframe/index.js';
@@ -321,10 +329,88 @@ export interface StrategySetup {
321
329
  export interface ScriptOptions {
322
330
  /** Creates the order engine of a strategy script; called by strategy(). */
323
331
  strategyEngine?: (setup: StrategySetup) => StrategyEngine;
332
+ /**
333
+ * Records the strategy engine's answers of each bar and the `varip` values, for the intrabar runs of a strategy
334
+ * (see {@link executeIntrabar}). The host keeps one recorder for the main run of a backtest and its intrabar runs.
335
+ */
336
+ recorder?: StrategyRecorder;
324
337
  }
325
338
 
326
339
  let scriptOptions: ScriptOptions = {};
327
340
 
341
+ /** Intrabar run of the current run (see {@link executeIntrabar}); null in a main run. */
342
+ interface IntrabarRun {
343
+ /** The bar calculated inside the bar: the last bar of the run. */
344
+ bar: number;
345
+ /** Bar index of the chart's last bar (`last_bar_index`, `barstate.islast`, `BarContext.n`). */
346
+ lastBarIndex: number;
347
+ engine: StrategyEngine;
348
+ recorder: StrategyRecorder;
349
+ }
350
+ let intrabarRun: IntrabarRun | null = null;
351
+
352
+ /**
353
+ * Answers of the strategy engine recorded in the main run of a strategy, bar by bar, and its `varip` values: an
354
+ * intrabar run replays the bars before its bar from them (see {@link executeIntrabar}). Create one per backtest and
355
+ * pass it to the main run (`ScriptOptions.recorder`) and to every intrabar run.
356
+ */
357
+ export class StrategyRecorder {
358
+ /** Per bar: the values the engine returned to the strategy.eachBar() callback (strategy.*, trades...), in order. */
359
+ readonly answers: unknown[][] = [];
360
+ /** Per bar: the `varip` values when the bar's strategy.eachBar() callback started in the main run. */
361
+ readonly varipStarts: unknown[][] = [];
362
+ /** Current `varip` values: kept through the intrabar runs (no rollback). */
363
+ readonly varip: unknown[] = [];
364
+ }
365
+
366
+ /** Module state of a run, saved around a nested run (an intrabar run inside the bar loop of the main run). */
367
+ interface RunState {
368
+ bars: Bar[];
369
+ ctx: Collector | null;
370
+ chartCtx: ChartContext;
371
+ runNow: number;
372
+ scriptOptions: ScriptOptions;
373
+ currentBar: number;
374
+ missingSymbols: SymbolDataRequest[];
375
+ sessionCache: typeof sessionCache;
376
+ intrabarRun: IntrabarRun | null;
377
+ varip: VaripRun;
378
+ drawings: ReturnType<typeof saveDrawingState>;
379
+ random: ReturnType<typeof mathCore.saveRandom>;
380
+ }
381
+
382
+ function saveRunState(): RunState {
383
+ return {
384
+ bars: ctxBars.bars,
385
+ ctx,
386
+ chartCtx,
387
+ runNow,
388
+ scriptOptions,
389
+ currentBar,
390
+ missingSymbols,
391
+ sessionCache,
392
+ intrabarRun,
393
+ varip: varipRun,
394
+ drawings: saveDrawingState(),
395
+ random: mathCore.saveRandom(),
396
+ };
397
+ }
398
+
399
+ function restoreRunState(st: RunState): void {
400
+ ctxBars.setAll(st.bars);
401
+ ctx = st.ctx;
402
+ chartCtx = st.chartCtx;
403
+ runNow = st.runNow;
404
+ scriptOptions = st.scriptOptions;
405
+ currentBar = st.currentBar;
406
+ missingSymbols = st.missingSymbols;
407
+ sessionCache = st.sessionCache;
408
+ intrabarRun = st.intrabarRun;
409
+ varipRun = st.varip;
410
+ restoreDrawingState(st.drawings);
411
+ mathCore.restoreRandom(st.random);
412
+ }
413
+
328
414
  /**
329
415
  * Runs a script body against `bars` with the given input overrides and
330
416
  * collects everything it declared. `chart` describes the bars (chart timeframe,
@@ -339,11 +425,16 @@ export function executeScript(
339
425
  chart: ChartContext = {},
340
426
  options: ScriptOptions = {}
341
427
  ): ScriptRunResult {
428
+ // A run inside another one (an intrabar run of a strategy) puts the outer run back when it ends.
429
+ const outer = ctx ? saveRunState() : null;
342
430
  ctxBars.setAll(barsInput);
343
431
  chartCtx = { ...chart };
344
432
  runNow = chart.now ?? Date.now();
345
433
  scriptOptions = options;
346
434
  ctx = freshCollector(inputs);
435
+ intrabarRun = null;
436
+ varipRun = { slot: 0, recorder: options.recorder ?? null, mode: 'live', scratch: null };
437
+ currentBar = -1;
347
438
  resetDrawings();
348
439
  mathCore.resetRandom();
349
440
  missingSymbols = [];
@@ -391,7 +482,110 @@ export function executeScript(
391
482
  };
392
483
  } finally {
393
484
  ctx = null;
485
+ if (outer) restoreRunState(outer);
486
+ }
487
+ }
488
+
489
+ /** What {@link executeIntrabar} needs besides the script. */
490
+ export interface IntrabarOptions {
491
+ /** The bar calculated inside the bar: `bars` ends with it, as it stands at the tick (close = the tick price). */
492
+ bar: number;
493
+ /** Bar index of the chart's last bar (the main run's last bar). */
494
+ lastBarIndex: number;
495
+ /** The strategy engine of the main run: the order calls and `strategy.*` reads of bar `bar` go to it. */
496
+ engine: StrategyEngine;
497
+ /** The recorder of the main run. */
498
+ recorder: StrategyRecorder;
499
+ }
500
+
501
+ /**
502
+ * Runs a strategy once more on a bar, inside the bar (PineScript `calc_on_order_fills` and calculations on the
503
+ * ticks of history bars): `bars` holds the bars up to that bar, the last one as it stands at the tick (its close is
504
+ * the tick price, its high and low the extremes reached so far). The script's values are calculated again on these
505
+ * bars, so its variables come back to their state at the start of the bar (PineScript rollback), except `varip`
506
+ * ones. In strategy.eachBar(), the bars before replay the engine's answers that the main run recorded (their
507
+ * order calls are dropped), and the bar itself calls `options.engine` without processBar / processClose (the host
508
+ * is filling its orders). That bar is a confirmed history bar (`barstate.isconfirmed`); `barstate.islast`, `last_bar_index` and
509
+ * `BarContext.n` keep the chart's last bar. Strategy risk rules are not declared again, and the outputs (plots,
510
+ * drawings) are dropped. Re-entrant: the host calls it inside the bar loop of the main run.
511
+ */
512
+ export function executeIntrabar(
513
+ body: () => void,
514
+ bars: Bar[],
515
+ inputs: Record<string, unknown>,
516
+ chart: ChartContext,
517
+ options: IntrabarOptions
518
+ ): void {
519
+ const run: IntrabarRun = {
520
+ bar: options.bar,
521
+ lastBarIndex: options.lastBarIndex,
522
+ engine: options.engine,
523
+ recorder: options.recorder,
524
+ };
525
+ executeScript(
526
+ () => {
527
+ intrabarRun = run;
528
+ varipRun = { slot: 0, recorder: options.recorder, mode: 'live', scratch: null };
529
+ body();
530
+ },
531
+ bars,
532
+ inputs,
533
+ // History bars are confirmed, also on their ticks (the reference app: a barstate.isconfirmed entry fills on them).
534
+ { ...chart, lastBarConfirmed: true, realtime: false },
535
+ { strategyEngine: () => options.engine }
536
+ );
537
+ }
538
+
539
+ // ── varip ────────────────────────────────────────────────────────────────────
540
+
541
+ /** How `varip` boxes read and write in the running code. */
542
+ interface VaripRun {
543
+ /** Next slot (boxes are numbered in their call order in the script). */
544
+ slot: number;
545
+ recorder: StrategyRecorder | null;
546
+ /** live: the recorder's current values; replay: the copy of a replayed bar's start values. */
547
+ mode: 'live' | 'replay';
548
+ scratch: unknown[] | null;
549
+ }
550
+ let varipRun: VaripRun = { slot: 0, recorder: null, mode: 'live', scratch: null };
551
+
552
+ /** A `varip` variable: `.value` reads and writes it. */
553
+ export interface VaripVariable<T> {
554
+ value: T;
555
+ }
556
+
557
+ /**
558
+ * PineScript `varip`: a variable that keeps the changes made in the intrabar runs of a strategy (PineScript
559
+ * rollback leaves it alone). A plain `let` variable of the script comes back to its value at the start of the bar
560
+ * in each intrabar run; a `varip` one does not. Declare it once in the script body (outside the callbacks), in the
561
+ * same order on each run; read and write it in strategy.eachBar(). Without intrabar runs it is a plain variable.
562
+ */
563
+ export function varip<T>(initial: T): VaripVariable<T> {
564
+ collector();
565
+ const slot = varipRun.slot++;
566
+ const state = varipRun;
567
+ const rec = state.recorder;
568
+ if (!rec) {
569
+ let current = initial;
570
+ return {
571
+ get value() {
572
+ return current;
573
+ },
574
+ set value(v: T) {
575
+ current = v;
576
+ },
577
+ };
394
578
  }
579
+ if (rec.varip.length <= slot) rec.varip[slot] = initial;
580
+ const store = (): unknown[] => (state.mode === 'replay' && state.scratch ? state.scratch : rec.varip);
581
+ return {
582
+ get value() {
583
+ return store()[slot] as T;
584
+ },
585
+ set value(v: T) {
586
+ store()[slot] = v;
587
+ },
588
+ };
395
589
  }
396
590
 
397
591
  // ── Declarations ─────────────────────────────────────────────────────────────
@@ -670,18 +864,39 @@ export const ta = {
670
864
  export function fixnan(source: Series): Series;
671
865
  export function fixnan(source: Array<string | undefined>): Array<string | undefined>;
672
866
  export function fixnan(source: Series | Array<string | undefined>): Series | Array<string | undefined> {
673
- return source instanceof Series ? Series.fromArray(ctxBars, fixnanValues(source.toArray())) : fixnanValues(source);
867
+ if (source instanceof Series) return Series.fromArray(ctxBars, fixnanValues(source.toArray()));
868
+ // the colours are the same set: the palette stays
869
+ return withPalette(fixnanValues(source), colorPalettes.get(source));
674
870
  }
675
871
 
676
872
  // ── Color helpers ────────────────────────────────────────────────────────────
677
873
 
874
+ /**
875
+ * The palette of a per-bar colour array: the constant colours it chooses from (PineScript compiles the colours of a
876
+ * conditional colour expression into a palette, in their order in the expression). An array without one (colours
877
+ * computed per bar) has none: its colours cannot be changed one by one.
878
+ */
879
+ const colorPalettes = new WeakMap<object, string[]>();
880
+
881
+ function withPalette<T extends object>(colors: T, palette: string[] | undefined): T {
882
+ if (palette?.length) colorPalettes.set(colors, palette);
883
+ return colors;
884
+ }
885
+
886
+ /** The palette of a colour argument given as a per-bar array, if it has one. */
887
+ function paletteOfColors(col: unknown): string[] | undefined {
888
+ return typeof col === 'object' && col !== null ? colorPalettes.get(col) : undefined;
889
+ }
890
+
678
891
  /** Per-bar color selection: truthy → colorTrue, else colorFalse.
679
892
  * colorFalse may be omitted (PineScript `na`): those bars get no color,
680
893
  * which bgcolor()/barcolor() skip. */
681
894
  function colorWhen(cond: Series, colorTrue: string, colorFalse: string): string[];
682
895
  function colorWhen(cond: Series, colorTrue: string, colorFalse?: string): Array<string | undefined>;
683
896
  function colorWhen(cond: Series, colorTrue: string, colorFalse?: string): Array<string | undefined> {
684
- return cond.toArray().map((v) => (!isNA(v) && v !== 0 ? colorTrue : colorFalse));
897
+ const colors = cond.toArray().map((v) => (!isNA(v) && v !== 0 ? colorTrue : colorFalse));
898
+ const palette = [colorTrue, ...(colorFalse !== undefined && colorFalse !== colorTrue ? [colorFalse] : [])];
899
+ return withPalette(colors, palette);
685
900
  }
686
901
 
687
902
  /** color.* plus `when` — per-bar conditional colors for plot()/fill()/bgcolor()/barcolor(). */
@@ -743,6 +958,7 @@ function plotImpl(series: Series, title: string = DEFAULT_OUTPUT_TITLES.plot, op
743
958
  histbase: options.histbase,
744
959
  forceOverlay: options.force_overlay,
745
960
  ...(options.editable === false ? { editable: false } : {}),
961
+ ...(paletteOfColors(perBar) ? { palette: paletteOfColors(perBar) } : {}),
746
962
  });
747
963
  c.outputs.push({ kind: 'plot', id, title });
748
964
  const values = series.toArray();
@@ -868,6 +1084,8 @@ export function fill(a: PlotHandle, b: PlotHandle, ...args: unknown[]): void {
868
1084
  let title: string | undefined;
869
1085
  let gradient: FillGradient | undefined;
870
1086
  let editable: unknown;
1087
+ /** The per-bar colour argument as given (its palette) */
1088
+ let perBarArg: unknown;
871
1089
  if (typeof third === 'number' || third instanceof Series) {
872
1090
  const n = ctxBars.bars.length;
873
1091
  gradient = {
@@ -881,12 +1099,14 @@ export function fill(a: PlotHandle, b: PlotHandle, ...args: unknown[]): void {
881
1099
  } else if (third !== null && typeof third === 'object' && !Array.isArray(third)) {
882
1100
  const options = third as ScriptFillOptions;
883
1101
  perBar = Array.isArray(options.color) ? options.color : undefined;
1102
+ perBarArg = perBar;
884
1103
  staticColor = typeof options.color === 'string' ? options.color : undefined;
885
1104
  title = options.title;
886
1105
  editable = options.editable;
887
1106
  } else {
888
1107
  const color = third as ScriptFillColor;
889
1108
  perBar = Array.isArray(color) ? color.map((x) => x ?? '') : undefined;
1109
+ perBarArg = Array.isArray(color) ? color : undefined;
890
1110
  staticColor = typeof color === 'string' ? color : undefined;
891
1111
  title = args[1] as string | undefined;
892
1112
  editable = args[2];
@@ -907,6 +1127,7 @@ export function fill(a: PlotHandle, b: PlotHandle, ...args: unknown[]): void {
907
1127
  ...(topColor ? { topColor } : {}),
908
1128
  ...(bottomColor ? { bottomColor } : {}),
909
1129
  ...(editable === false ? { editable: false } : {}),
1130
+ ...(paletteOfColors(perBarArg) ? { palette: paletteOfColors(perBarArg) } : {}),
910
1131
  });
911
1132
  if (hlines) return;
912
1133
  c.fills.push({
@@ -1132,8 +1353,8 @@ function emitBarColors(
1132
1353
  title: options.title ?? DEFAULT_OUTPUT_TITLES[kind],
1133
1354
  offset: options.offset,
1134
1355
  forceOverlay: options.force_overlay,
1135
- // a constant colour is a palette of one colour (PineScript)
1136
- ...(typeof colors === 'string' ? { palette: [colors] } : {}),
1356
+ // a constant colour is a palette of one colour (PineScript); per-bar colours from constants keep theirs
1357
+ ...(typeof colors === 'string' ? { palette: [colors] } : paletteOfColors(colors) ? { palette: paletteOfColors(colors) } : {}),
1137
1358
  ...(options.editable === false ? { editable: false } : {}),
1138
1359
  outputIndex: c.outputs.length,
1139
1360
  });
@@ -1232,6 +1453,16 @@ const staticColor = (col: ScriptCandleColor | undefined): string | undefined =>
1232
1453
  */
1233
1454
  export const DEFAULT_CANDLE_COLORS = { color: '#2962FF', wickColor: '#737375', borderColor: '#000000' } as const;
1234
1455
 
1456
+ /** The palettes of the per-bar colour arguments of a plotcandle / plotbar that have one. */
1457
+ function candlePalettes(cols: { color?: ScriptCandleColor; wickColor?: ScriptCandleColor; borderColor?: ScriptCandleColor }): { palettes?: CandleConfig['palettes'] } {
1458
+ const palettes: NonNullable<CandleConfig['palettes']> = {};
1459
+ for (const k of ['color', 'wickColor', 'borderColor'] as const) {
1460
+ const pal = paletteOfColors(cols[k]);
1461
+ if (pal) palettes[k] = pal;
1462
+ }
1463
+ return Object.keys(palettes).length ? { palettes } : {};
1464
+ }
1465
+
1235
1466
  /** A colour argument, or the default when it is left out. */
1236
1467
  const orDefault = (col: ScriptCandleColor | undefined, fallback: string): ScriptCandleColor => (col === undefined ? fallback : col);
1237
1468
 
@@ -1260,6 +1491,7 @@ export function plotcandle(
1260
1491
  color: staticColor(color),
1261
1492
  wickColor: staticColor(wickcolor),
1262
1493
  borderColor: staticColor(bordercolor),
1494
+ ...candlePalettes({ color, wickColor: wickcolor, borderColor: bordercolor }),
1263
1495
  display: options.display,
1264
1496
  forceOverlay: options.force_overlay,
1265
1497
  ...(options.editable === false ? { editable: false } : {}),
@@ -1300,6 +1532,7 @@ export function plotbar(
1300
1532
  kind: 'bar',
1301
1533
  title,
1302
1534
  color: staticColor(barColor),
1535
+ ...candlePalettes({ color: barColor }),
1303
1536
  display: options.display,
1304
1537
  forceOverlay: options.force_overlay,
1305
1538
  ...(options.editable === false ? { editable: false } : {}),
@@ -1379,6 +1612,8 @@ interface HistoryBuffer {
1379
1612
  export function eachBar(fn: (c: BarContext) => number | boolean | void): Series {
1380
1613
  collector(); // same lifecycle guard (and error message) as the declaration calls
1381
1614
  const b = bars();
1615
+ // An intrabar run ends on its bar: the chart's last bar stays the main run's.
1616
+ const lastIndex = intrabarRun ? intrabarRun.lastBarIndex : b.length - 1;
1382
1617
  const out: number[] = new Array(b.length).fill(NaN);
1383
1618
  const cache = new Map<Series, number[]>();
1384
1619
  const buffers = new Map<Series, HistoryBuffer>();
@@ -1388,7 +1623,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1388
1623
  get i() {
1389
1624
  return idx;
1390
1625
  },
1391
- n: b.length,
1626
+ n: lastIndex + 1,
1392
1627
  get time() {
1393
1628
  return b[idx]!.time as number;
1394
1629
  },
@@ -1433,7 +1668,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1433
1668
  for (idx = 0; idx < b.length; idx++) {
1434
1669
  if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
1435
1670
  currentBar = idx;
1436
- setDrawingBar(idx, idx === b.length - 1, b.length - 1);
1671
+ setDrawingBar(idx, idx === lastIndex, lastIndex);
1437
1672
  const r = fn(ctx);
1438
1673
  out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
1439
1674
  // grow the history buffers from the largest reference of this bar
@@ -1513,6 +1748,7 @@ function strategyVariable(name: StrategyVariable): number | string {
1513
1748
  }
1514
1749
 
1515
1750
  function riskRule(rule: StrategyRiskRule): void {
1751
+ if (intrabarRun) return; // declared by the main run
1516
1752
  const engine = strategyEngine(`strategy.risk.${rule.rule}()`);
1517
1753
  if (!engine.risk) throw new Error(`strategy.risk.${rule.rule}() is not provided by the strategy engine.`);
1518
1754
  engine.risk(rule);
@@ -1559,18 +1795,103 @@ function strategyEachBar(fn: (c: BarContext) => number | boolean | void): Series
1559
1795
  const engine = strategyEngine('strategy.eachBar()');
1560
1796
  if (run.looped) throw new Error('strategy.eachBar() runs once per script run.');
1561
1797
  run.looped = true;
1798
+ const intrabar = intrabarRun;
1799
+ if (intrabar) {
1800
+ // Intrabar run: the bars before replay the main run's answers, the bar itself calls the engine (whose
1801
+ // orders the host is filling: no processBar / processClose).
1802
+ const varipState = varipRun;
1803
+ return eachBar((c) => {
1804
+ const replay = c.i < intrabar.bar;
1805
+ run.engine = replay ? replayEngine(intrabar.recorder.answers[c.i] ?? []) : intrabar.engine;
1806
+ varipState.mode = replay ? 'replay' : 'live';
1807
+ varipState.scratch = replay ? [...(intrabar.recorder.varipStarts[c.i] ?? intrabar.recorder.varip)] : null;
1808
+ run.inLoop = true;
1809
+ try {
1810
+ return fn(c);
1811
+ } finally {
1812
+ run.inLoop = false;
1813
+ run.engine = intrabar.engine;
1814
+ varipState.mode = 'live';
1815
+ varipState.scratch = null;
1816
+ }
1817
+ });
1818
+ }
1819
+ const recorder = scriptOptions.recorder;
1562
1820
  return eachBar((c) => {
1563
1821
  engine.processBar(c.i);
1822
+ if (recorder) {
1823
+ recorder.varipStarts[c.i] = [...recorder.varip];
1824
+ run.engine = recordingEngine(engine, (recorder.answers[c.i] = []));
1825
+ }
1564
1826
  run.inLoop = true;
1565
1827
  try {
1566
1828
  return fn(c);
1567
1829
  } finally {
1568
1830
  run.inLoop = false;
1831
+ run.engine = engine;
1569
1832
  engine.processClose(c.i);
1570
1833
  }
1571
1834
  });
1572
1835
  }
1573
1836
 
1837
+ /** The engine of a main run that records its answers (see {@link StrategyRecorder}). */
1838
+ function recordingEngine(engine: StrategyEngine, log: unknown[]): StrategyEngine {
1839
+ const keep = <T>(v: T): T => {
1840
+ log.push(v);
1841
+ return v;
1842
+ };
1843
+ return {
1844
+ ...forward(engine),
1845
+ get: (name) => keep(engine.get(name)),
1846
+ openTrade: (index) => keep(engine.openTrade(index)),
1847
+ closedTrade: (index) => keep(engine.closedTrade(index)),
1848
+ defaultEntryQty: engine.defaultEntryQty ? (price) => keep(engine.defaultEntryQty!(price)) : undefined,
1849
+ };
1850
+ }
1851
+
1852
+ /** An engine that gives back recorded answers in order and drops the order calls (a replayed bar). */
1853
+ function replayEngine(log: readonly unknown[]): StrategyEngine {
1854
+ let k = 0;
1855
+ const next = <T>(): T => log[k++] as T;
1856
+ const none = (): void => {};
1857
+ return {
1858
+ processBar: none,
1859
+ processClose: none,
1860
+ entry: none,
1861
+ order: none,
1862
+ exit: none,
1863
+ close: none,
1864
+ close_all: none,
1865
+ cancel: none,
1866
+ cancel_all: none,
1867
+ risk: none,
1868
+ get: () => next(),
1869
+ openTrade: () => next(),
1870
+ closedTrade: () => next(),
1871
+ defaultEntryQty: () => next(),
1872
+ };
1873
+ }
1874
+
1875
+ /** The calls of `engine`, bound to it. */
1876
+ function forward(engine: StrategyEngine): StrategyEngine {
1877
+ return {
1878
+ processBar: (bar) => engine.processBar(bar),
1879
+ processClose: (bar) => engine.processClose(bar),
1880
+ entry: (id, direction, options) => engine.entry(id, direction, options),
1881
+ order: (id, direction, options) => engine.order(id, direction, options),
1882
+ exit: (id, options) => engine.exit(id, options),
1883
+ close: (id, options) => engine.close(id, options),
1884
+ close_all: (options) => engine.close_all(options),
1885
+ cancel: (id) => engine.cancel(id),
1886
+ cancel_all: () => engine.cancel_all(),
1887
+ risk: engine.risk ? (rule) => engine.risk!(rule) : undefined,
1888
+ get: (name) => engine.get(name),
1889
+ openTrade: (index) => engine.openTrade(index),
1890
+ closedTrade: (index) => engine.closedTrade(index),
1891
+ defaultEntryQty: engine.defaultEntryQty ? (price) => engine.defaultEntryQty!(price) : undefined,
1892
+ };
1893
+ }
1894
+
1574
1895
  /** Numeric `strategy.*` variables, read from the engine (current bar inside strategy.eachBar()). */
1575
1896
  export type StrategyNumberVariables = { readonly [K in StrategyNumberVariable]: number };
1576
1897
  /** Text `strategy.*` variables, read from the engine. */
@@ -2156,6 +2477,9 @@ function barFlag(test: (i: number, n: number) => boolean): Series {
2156
2477
  return Series.fromArray(ctxBars, Array.from({ length: n }, (_, i) => (test(i, n) ? 1 : 0)));
2157
2478
  }
2158
2479
 
2480
+ /** Number of bars of the chart: an intrabar run ends before the chart's last bar. */
2481
+ const chartBarCount = (n: number): number => (intrabarRun ? intrabarRun.lastBarIndex + 1 : n);
2482
+
2159
2483
  const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.realtime === true;
2160
2484
 
2161
2485
  /**
@@ -2167,7 +2491,7 @@ const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.
2167
2491
  */
2168
2492
  export const barstate = {
2169
2493
  get isfirst(): Series { return barFlag((i) => i === 0); },
2170
- get islast(): Series { return barFlag((i, n) => i === n - 1); },
2494
+ get islast(): Series { return barFlag((i, n) => i === chartBarCount(n) - 1); },
2171
2495
  get isconfirmed(): Series { return barFlag((i, n) => i < n - 1 || chartCtx.lastBarConfirmed === true); },
2172
2496
  get isrealtime(): Series { return barFlag(lastRealtime); },
2173
2497
  get ishistory(): Series { return barFlag((i, n) => !lastRealtime(i, n)); },
@@ -2175,7 +2499,8 @@ export const barstate = {
2175
2499
  get islastconfirmedhistory(): Series {
2176
2500
  return barFlag((i, n) => {
2177
2501
  const lastConfirmed = chartCtx.lastBarConfirmed === true && chartCtx.realtime !== true;
2178
- return i === (lastConfirmed ? n - 1 : n - 2);
2502
+ const count = chartBarCount(n);
2503
+ return i === (lastConfirmed ? count - 1 : count - 2);
2179
2504
  });
2180
2505
  },
2181
2506
  };
@@ -2619,8 +2944,11 @@ export type {
2619
2944
  } from '../strategy/index.js';
2620
2945
 
2621
2946
  /** PineScript `math.*`; `math.round_to_mintick(x)` uses `syminfo.mintick` of the chart context. */
2947
+ const { saveRandom: _saveRandom, restoreRandom: _restoreRandom, ...mathScript } = mathCore;
2948
+ void _saveRandom;
2949
+ void _restoreRandom;
2622
2950
  export const math = {
2623
- ...mathCore,
2951
+ ...mathScript,
2624
2952
  round_to_mintick: ((number: number | Series, mintick?: number) => {
2625
2953
  const tick = mintick ?? syminfo.mintick;
2626
2954
  return number instanceof Series ? mathCore.round_to_mintick(number, tick) : mathCore.round_to_mintick(number, tick);
@@ -43,6 +43,8 @@ export interface StrategyOptions {
43
43
  pyramiding?: number;
44
44
  calc_on_order_fills?: boolean;
45
45
  calc_on_every_tick?: boolean;
46
+ /** Runs the strategy on each tick of the history bars too (the reference app's "On history bar tick"). */
47
+ calc_on_every_history_tick?: boolean;
46
48
  max_bars_back?: number;
47
49
  backtest_fill_limits_assumption?: number;
48
50
  default_qty_type?: StrategyQtyType;
@@ -74,6 +76,7 @@ export interface StrategyProperties {
74
76
  pyramiding: number;
75
77
  calc_on_order_fills: boolean;
76
78
  calc_on_every_tick: boolean;
79
+ calc_on_every_history_tick: boolean;
77
80
  backtest_fill_limits_assumption: number;
78
81
  default_qty_type: StrategyQtyType;
79
82
  default_qty_value: number;
@@ -98,6 +101,7 @@ export const STRATEGY_DEFAULTS: StrategyProperties = {
98
101
  pyramiding: 0,
99
102
  calc_on_order_fills: false,
100
103
  calc_on_every_tick: false,
104
+ calc_on_every_history_tick: false,
101
105
  backtest_fill_limits_assumption: 0,
102
106
  // v6 values; PineScript v5: fixed / 1 / 1000000 and margins 0
103
107
  default_qty_type: 'percent_of_equity',