oakscriptjs 0.12.3 → 0.13.1

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 (37) 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/math/index.d.ts +4 -0
  6. package/dist/cjs/math/index.d.ts.map +1 -1
  7. package/dist/cjs/math/index.js +9 -0
  8. package/dist/cjs/math/index.js.map +2 -2
  9. package/dist/cjs/script/index.d.ts +52 -1
  10. package/dist/cjs/script/index.d.ts.map +1 -1
  11. package/dist/cjs/script/index.js +222 -12
  12. package/dist/cjs/script/index.js.map +3 -3
  13. package/dist/cjs/strategy/index.d.ts +3 -0
  14. package/dist/cjs/strategy/index.d.ts.map +1 -1
  15. package/dist/cjs/strategy/index.js +1 -0
  16. package/dist/cjs/strategy/index.js.map +2 -2
  17. package/dist/esm/drawing/registry.d.ts +37 -0
  18. package/dist/esm/drawing/registry.d.ts.map +1 -1
  19. package/dist/esm/drawing/registry.js +41 -0
  20. package/dist/esm/drawing/registry.js.map +1 -1
  21. package/dist/esm/math/index.d.ts +4 -0
  22. package/dist/esm/math/index.d.ts.map +1 -1
  23. package/dist/esm/math/index.js +10 -0
  24. package/dist/esm/math/index.js.map +1 -1
  25. package/dist/esm/script/index.d.ts +52 -1
  26. package/dist/esm/script/index.d.ts.map +1 -1
  27. package/dist/esm/script/index.js +263 -13
  28. package/dist/esm/script/index.js.map +1 -1
  29. package/dist/esm/strategy/index.d.ts +3 -0
  30. package/dist/esm/strategy/index.d.ts.map +1 -1
  31. package/dist/esm/strategy/index.js +1 -0
  32. package/dist/esm/strategy/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/drawing/registry.ts +56 -0
  35. package/src/math/index.ts +11 -0
  36. package/src/script/index.ts +339 -13
  37. 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,91 @@ 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
+ /** The `varip` values of a replayed bar in a script without `varip`. */
342
+ const NO_VARIP: unknown[] = [];
343
+
344
+ /** Intrabar run of the current run (see {@link executeIntrabar}); null in a main run. */
345
+ interface IntrabarRun {
346
+ /** The bar calculated inside the bar: the last bar of the run. */
347
+ bar: number;
348
+ /** Bar index of the chart's last bar (`last_bar_index`, `barstate.islast`, `BarContext.n`). */
349
+ lastBarIndex: number;
350
+ engine: StrategyEngine;
351
+ recorder: StrategyRecorder;
352
+ }
353
+ let intrabarRun: IntrabarRun | null = null;
354
+
355
+ /**
356
+ * Answers of the strategy engine recorded in the main run of a strategy, bar by bar, and its `varip` values: an
357
+ * intrabar run replays the bars before its bar from them (see {@link executeIntrabar}). Create one per backtest and
358
+ * pass it to the main run (`ScriptOptions.recorder`) and to every intrabar run.
359
+ */
360
+ export class StrategyRecorder {
361
+ /** Per bar: the values the engine returned to the strategy.eachBar() callback (strategy.*, trades...), in order. */
362
+ readonly answers: unknown[][] = [];
363
+ /** Per bar: the `varip` values when the bar's strategy.eachBar() callback started in the main run. */
364
+ readonly varipStarts: unknown[][] = [];
365
+ /** Current `varip` values: kept through the intrabar runs (no rollback). */
366
+ readonly varip: unknown[] = [];
367
+ }
368
+
369
+ /** Module state of a run, saved around a nested run (an intrabar run inside the bar loop of the main run). */
370
+ interface RunState {
371
+ bars: Bar[];
372
+ ctx: Collector | null;
373
+ chartCtx: ChartContext;
374
+ runNow: number;
375
+ scriptOptions: ScriptOptions;
376
+ currentBar: number;
377
+ missingSymbols: SymbolDataRequest[];
378
+ sessionCache: typeof sessionCache;
379
+ intrabarRun: IntrabarRun | null;
380
+ varip: VaripRun;
381
+ drawings: ReturnType<typeof saveDrawingState>;
382
+ random: ReturnType<typeof mathCore.saveRandom>;
383
+ }
384
+
385
+ function saveRunState(): RunState {
386
+ return {
387
+ bars: ctxBars.bars,
388
+ ctx,
389
+ chartCtx,
390
+ runNow,
391
+ scriptOptions,
392
+ currentBar,
393
+ missingSymbols,
394
+ sessionCache,
395
+ intrabarRun,
396
+ varip: varipRun,
397
+ drawings: saveDrawingState(),
398
+ random: mathCore.saveRandom(),
399
+ };
400
+ }
401
+
402
+ function restoreRunState(st: RunState): void {
403
+ ctxBars.setAll(st.bars);
404
+ ctx = st.ctx;
405
+ chartCtx = st.chartCtx;
406
+ runNow = st.runNow;
407
+ scriptOptions = st.scriptOptions;
408
+ currentBar = st.currentBar;
409
+ missingSymbols = st.missingSymbols;
410
+ sessionCache = st.sessionCache;
411
+ intrabarRun = st.intrabarRun;
412
+ varipRun = st.varip;
413
+ restoreDrawingState(st.drawings);
414
+ mathCore.restoreRandom(st.random);
415
+ }
416
+
328
417
  /**
329
418
  * Runs a script body against `bars` with the given input overrides and
330
419
  * collects everything it declared. `chart` describes the bars (chart timeframe,
@@ -339,11 +428,16 @@ export function executeScript(
339
428
  chart: ChartContext = {},
340
429
  options: ScriptOptions = {}
341
430
  ): ScriptRunResult {
431
+ // A run inside another one (an intrabar run of a strategy) puts the outer run back when it ends.
432
+ const outer = ctx ? saveRunState() : null;
342
433
  ctxBars.setAll(barsInput);
343
434
  chartCtx = { ...chart };
344
435
  runNow = chart.now ?? Date.now();
345
436
  scriptOptions = options;
346
437
  ctx = freshCollector(inputs);
438
+ intrabarRun = null;
439
+ varipRun = { slot: 0, recorder: options.recorder ?? null, mode: 'live', scratch: null };
440
+ currentBar = -1;
347
441
  resetDrawings();
348
442
  mathCore.resetRandom();
349
443
  missingSymbols = [];
@@ -383,7 +477,7 @@ export function executeScript(
383
477
  barColors: c.barColors.length ? c.barColors : undefined,
384
478
  plotCandles: Object.keys(c.plotCandles).length ? c.plotCandles : undefined,
385
479
  plotBars: Object.keys(c.plotBars).length ? c.plotBars : undefined,
386
- ...drawingOutputs(ctxBars.bars, chartCtx.timeUnit),
480
+ ...(intrabarRun ? {} : drawingOutputs(ctxBars.bars, chartCtx.timeUnit)),
387
481
  alerts: c.alerts.length ? c.alerts : undefined,
388
482
  alertCalls: c.alertCalls.length ? c.alertCalls : undefined,
389
483
  },
@@ -391,7 +485,110 @@ export function executeScript(
391
485
  };
392
486
  } finally {
393
487
  ctx = null;
488
+ if (outer) restoreRunState(outer);
489
+ }
490
+ }
491
+
492
+ /** What {@link executeIntrabar} needs besides the script. */
493
+ export interface IntrabarOptions {
494
+ /** The bar calculated inside the bar: `bars` ends with it, as it stands at the tick (close = the tick price). */
495
+ bar: number;
496
+ /** Bar index of the chart's last bar (the main run's last bar). */
497
+ lastBarIndex: number;
498
+ /** The strategy engine of the main run: the order calls and `strategy.*` reads of bar `bar` go to it. */
499
+ engine: StrategyEngine;
500
+ /** The recorder of the main run. */
501
+ recorder: StrategyRecorder;
502
+ }
503
+
504
+ /**
505
+ * Runs a strategy once more on a bar, inside the bar (PineScript `calc_on_order_fills` and calculations on the
506
+ * ticks of history bars): `bars` holds the bars up to that bar, the last one as it stands at the tick (its close is
507
+ * the tick price, its high and low the extremes reached so far). The script's values are calculated again on these
508
+ * bars, so its variables come back to their state at the start of the bar (PineScript rollback), except `varip`
509
+ * ones. In strategy.eachBar(), the bars before replay the engine's answers that the main run recorded (their
510
+ * order calls are dropped), and the bar itself calls `options.engine` without processBar / processClose (the host
511
+ * is filling its orders). That bar is a confirmed history bar (`barstate.isconfirmed`); `barstate.islast`, `last_bar_index` and
512
+ * `BarContext.n` keep the chart's last bar. Strategy risk rules are not declared again, and the outputs (plots,
513
+ * drawings) are dropped. Re-entrant: the host calls it inside the bar loop of the main run.
514
+ */
515
+ export function executeIntrabar(
516
+ body: () => void,
517
+ bars: Bar[],
518
+ inputs: Record<string, unknown>,
519
+ chart: ChartContext,
520
+ options: IntrabarOptions
521
+ ): void {
522
+ const run: IntrabarRun = {
523
+ bar: options.bar,
524
+ lastBarIndex: options.lastBarIndex,
525
+ engine: options.engine,
526
+ recorder: options.recorder,
527
+ };
528
+ executeScript(
529
+ () => {
530
+ intrabarRun = run;
531
+ varipRun = { slot: 0, recorder: options.recorder, mode: 'live', scratch: null };
532
+ body();
533
+ },
534
+ bars,
535
+ inputs,
536
+ // History bars are confirmed, also on their ticks (the reference app: a barstate.isconfirmed entry fills on them).
537
+ { ...chart, lastBarConfirmed: true, realtime: false },
538
+ { strategyEngine: () => options.engine }
539
+ );
540
+ }
541
+
542
+ // ── varip ────────────────────────────────────────────────────────────────────
543
+
544
+ /** How `varip` boxes read and write in the running code. */
545
+ interface VaripRun {
546
+ /** Next slot (boxes are numbered in their call order in the script). */
547
+ slot: number;
548
+ recorder: StrategyRecorder | null;
549
+ /** live: the recorder's current values; replay: the copy of a replayed bar's start values. */
550
+ mode: 'live' | 'replay';
551
+ scratch: unknown[] | null;
552
+ }
553
+ let varipRun: VaripRun = { slot: 0, recorder: null, mode: 'live', scratch: null };
554
+
555
+ /** A `varip` variable: `.value` reads and writes it. */
556
+ export interface VaripVariable<T> {
557
+ value: T;
558
+ }
559
+
560
+ /**
561
+ * PineScript `varip`: a variable that keeps the changes made in the intrabar runs of a strategy (PineScript
562
+ * rollback leaves it alone). A plain `let` variable of the script comes back to its value at the start of the bar
563
+ * in each intrabar run; a `varip` one does not. Declare it once in the script body (outside the callbacks), in the
564
+ * same order on each run; read and write it in strategy.eachBar(). Without intrabar runs it is a plain variable.
565
+ */
566
+ export function varip<T>(initial: T): VaripVariable<T> {
567
+ collector();
568
+ const slot = varipRun.slot++;
569
+ const state = varipRun;
570
+ const rec = state.recorder;
571
+ if (!rec) {
572
+ let current = initial;
573
+ return {
574
+ get value() {
575
+ return current;
576
+ },
577
+ set value(v: T) {
578
+ current = v;
579
+ },
580
+ };
394
581
  }
582
+ if (rec.varip.length <= slot) rec.varip[slot] = initial;
583
+ const store = (): unknown[] => (state.mode === 'replay' && state.scratch ? state.scratch : rec.varip);
584
+ return {
585
+ get value() {
586
+ return store()[slot] as T;
587
+ },
588
+ set value(v: T) {
589
+ store()[slot] = v;
590
+ },
591
+ };
395
592
  }
396
593
 
397
594
  // ── Declarations ─────────────────────────────────────────────────────────────
@@ -749,6 +946,7 @@ export const DEFAULT_OUTPUT_TITLES = {
749
946
 
750
947
  /** PineScript `plot()` — declares the plot and supplies its data in one call. */
751
948
  function plotImpl(series: Series, title: string = DEFAULT_OUTPUT_TITLES.plot, options: ScriptPlotOptions = {}): PlotHandle {
949
+ if (intrabarRun) return { id: '', kind: 'plot' }; // dropped: an intrabar run's outputs aren't read
752
950
  const c = collector();
753
951
  const id = `plot${c.plotConfig.length}`;
754
952
  const perBar = Array.isArray(options.color) ? options.color : undefined;
@@ -883,6 +1081,7 @@ export function fill(
883
1081
  ...rest: unknown[]
884
1082
  ): void;
885
1083
  export function fill(a: PlotHandle, b: PlotHandle, ...args: unknown[]): void {
1084
+ if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
886
1085
  const c = collector();
887
1086
  const third = args[0];
888
1087
  let perBar: string[] | undefined;
@@ -1025,6 +1224,7 @@ function emitMarkers(
1025
1224
  title: string | undefined,
1026
1225
  options: PlotShapeOptions & PlotCharOptions
1027
1226
  ): void {
1227
+ if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
1028
1228
  const c = collector();
1029
1229
  const id = `${kind}${c.shapeConfig.filter((s) => s.kind === kind).length}`;
1030
1230
  title ??= DEFAULT_OUTPUT_TITLES[kind];
@@ -1108,6 +1308,7 @@ export interface PlotArrowOptions {
1108
1308
  * `arrowConfig`; the renderer scales the height with the absolute value between minheight and maxheight.
1109
1309
  */
1110
1310
  export function plotarrow(series: Series, title?: string, options: PlotArrowOptions = {}): void {
1311
+ if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
1111
1312
  const c = collector();
1112
1313
  const id = `arrow${c.arrowConfig.length}`;
1113
1314
  title ??= DEFAULT_OUTPUT_TITLES.arrow;
@@ -1151,6 +1352,7 @@ function emitBarColors(
1151
1352
  colors: string | Array<string | undefined>,
1152
1353
  options: { offset?: number; title?: string; force_overlay?: boolean; editable?: boolean }
1153
1354
  ): void {
1355
+ if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
1154
1356
  const c = collector();
1155
1357
  const id = `${kind}${c.barColorConfig.filter((x) => x.kind === kind).length}`;
1156
1358
  c.barColorConfig.push({
@@ -1284,6 +1486,7 @@ export function plotcandle(
1284
1486
  title?: string,
1285
1487
  options: PlotCandleOptions = {}
1286
1488
  ): void {
1489
+ if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
1287
1490
  const c = collector();
1288
1491
  const color = orDefault(options.color, DEFAULT_CANDLE_COLORS.color);
1289
1492
  const wickcolor = orDefault(options.wickcolor, DEFAULT_CANDLE_COLORS.wickColor);
@@ -1329,6 +1532,7 @@ export function plotbar(
1329
1532
  title?: string,
1330
1533
  options: PlotBarOptions = {}
1331
1534
  ): void {
1535
+ if (intrabarRun) return; // dropped: an intrabar run's outputs aren't read
1332
1536
  const c = collector();
1333
1537
  const barColor = orDefault(options.color, DEFAULT_CANDLE_COLORS.color);
1334
1538
  const id = `plotbar${c.candleConfig.filter((x) => x.kind === 'bar').length}`;
@@ -1418,6 +1622,8 @@ interface HistoryBuffer {
1418
1622
  export function eachBar(fn: (c: BarContext) => number | boolean | void): Series {
1419
1623
  collector(); // same lifecycle guard (and error message) as the declaration calls
1420
1624
  const b = bars();
1625
+ // An intrabar run ends on its bar: the chart's last bar stays the main run's.
1626
+ const lastIndex = intrabarRun ? intrabarRun.lastBarIndex : b.length - 1;
1421
1627
  const out: number[] = new Array(b.length).fill(NaN);
1422
1628
  const cache = new Map<Series, number[]>();
1423
1629
  const buffers = new Map<Series, HistoryBuffer>();
@@ -1427,7 +1633,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1427
1633
  get i() {
1428
1634
  return idx;
1429
1635
  },
1430
- n: b.length,
1636
+ n: lastIndex + 1,
1431
1637
  get time() {
1432
1638
  return b[idx]!.time as number;
1433
1639
  },
@@ -1472,7 +1678,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1472
1678
  for (idx = 0; idx < b.length; idx++) {
1473
1679
  if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
1474
1680
  currentBar = idx;
1475
- setDrawingBar(idx, idx === b.length - 1, b.length - 1);
1681
+ setDrawingBar(idx, idx === lastIndex, lastIndex);
1476
1682
  const r = fn(ctx);
1477
1683
  out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
1478
1684
  // grow the history buffers from the largest reference of this bar
@@ -1552,6 +1758,7 @@ function strategyVariable(name: StrategyVariable): number | string {
1552
1758
  }
1553
1759
 
1554
1760
  function riskRule(rule: StrategyRiskRule): void {
1761
+ if (intrabarRun) return; // declared by the main run
1555
1762
  const engine = strategyEngine(`strategy.risk.${rule.rule}()`);
1556
1763
  if (!engine.risk) throw new Error(`strategy.risk.${rule.rule}() is not provided by the strategy engine.`);
1557
1764
  engine.risk(rule);
@@ -1598,18 +1805,109 @@ function strategyEachBar(fn: (c: BarContext) => number | boolean | void): Series
1598
1805
  const engine = strategyEngine('strategy.eachBar()');
1599
1806
  if (run.looped) throw new Error('strategy.eachBar() runs once per script run.');
1600
1807
  run.looped = true;
1808
+ const intrabar = intrabarRun;
1809
+ if (intrabar) {
1810
+ // Intrabar run: the bars before replay the main run's answers, the bar itself calls the engine (whose
1811
+ // orders the host is filling: no processBar / processClose).
1812
+ const varipState = varipRun;
1813
+ const replayLog: { answers: readonly unknown[]; next: number } = { answers: [], next: 0 };
1814
+ const replayed = replayEngine(replayLog);
1815
+ return eachBar((c) => {
1816
+ const replay = c.i < intrabar.bar;
1817
+ if (replay) {
1818
+ replayLog.answers = intrabar.recorder.answers[c.i] ?? [];
1819
+ replayLog.next = 0;
1820
+ }
1821
+ run.engine = replay ? replayed : intrabar.engine;
1822
+ varipState.mode = replay ? 'replay' : 'live';
1823
+ const starts = replay ? intrabar.recorder.varipStarts[c.i] ?? intrabar.recorder.varip : null;
1824
+ varipState.scratch = starts ? (starts.length ? [...starts] : NO_VARIP) : null;
1825
+ run.inLoop = true;
1826
+ try {
1827
+ return fn(c);
1828
+ } finally {
1829
+ run.inLoop = false;
1830
+ run.engine = intrabar.engine;
1831
+ varipState.mode = 'live';
1832
+ varipState.scratch = null;
1833
+ }
1834
+ });
1835
+ }
1836
+ const recorder = scriptOptions.recorder;
1601
1837
  return eachBar((c) => {
1602
1838
  engine.processBar(c.i);
1839
+ if (recorder) {
1840
+ recorder.varipStarts[c.i] = [...recorder.varip];
1841
+ run.engine = recordingEngine(engine, (recorder.answers[c.i] = []));
1842
+ }
1603
1843
  run.inLoop = true;
1604
1844
  try {
1605
1845
  return fn(c);
1606
1846
  } finally {
1607
1847
  run.inLoop = false;
1848
+ run.engine = engine;
1608
1849
  engine.processClose(c.i);
1609
1850
  }
1610
1851
  });
1611
1852
  }
1612
1853
 
1854
+ /** The engine of a main run that records its answers (see {@link StrategyRecorder}). */
1855
+ function recordingEngine(engine: StrategyEngine, log: unknown[]): StrategyEngine {
1856
+ const keep = <T>(v: T): T => {
1857
+ log.push(v);
1858
+ return v;
1859
+ };
1860
+ return {
1861
+ ...forward(engine),
1862
+ get: (name) => keep(engine.get(name)),
1863
+ openTrade: (index) => keep(engine.openTrade(index)),
1864
+ closedTrade: (index) => keep(engine.closedTrade(index)),
1865
+ defaultEntryQty: engine.defaultEntryQty ? (price) => keep(engine.defaultEntryQty!(price)) : undefined,
1866
+ };
1867
+ }
1868
+
1869
+ /** An engine that gives back recorded answers in order and drops the order calls (a replayed bar). */
1870
+ function replayEngine(log: { answers: readonly unknown[]; next: number }): StrategyEngine {
1871
+ const next = <T>(): T => log.answers[log.next++] as T;
1872
+ const none = (): void => {};
1873
+ return {
1874
+ processBar: none,
1875
+ processClose: none,
1876
+ entry: none,
1877
+ order: none,
1878
+ exit: none,
1879
+ close: none,
1880
+ close_all: none,
1881
+ cancel: none,
1882
+ cancel_all: none,
1883
+ risk: none,
1884
+ get: () => next(),
1885
+ openTrade: () => next(),
1886
+ closedTrade: () => next(),
1887
+ defaultEntryQty: () => next(),
1888
+ };
1889
+ }
1890
+
1891
+ /** The calls of `engine`, bound to it. */
1892
+ function forward(engine: StrategyEngine): StrategyEngine {
1893
+ return {
1894
+ processBar: (bar) => engine.processBar(bar),
1895
+ processClose: (bar) => engine.processClose(bar),
1896
+ entry: (id, direction, options) => engine.entry(id, direction, options),
1897
+ order: (id, direction, options) => engine.order(id, direction, options),
1898
+ exit: (id, options) => engine.exit(id, options),
1899
+ close: (id, options) => engine.close(id, options),
1900
+ close_all: (options) => engine.close_all(options),
1901
+ cancel: (id) => engine.cancel(id),
1902
+ cancel_all: () => engine.cancel_all(),
1903
+ risk: engine.risk ? (rule) => engine.risk!(rule) : undefined,
1904
+ get: (name) => engine.get(name),
1905
+ openTrade: (index) => engine.openTrade(index),
1906
+ closedTrade: (index) => engine.closedTrade(index),
1907
+ defaultEntryQty: engine.defaultEntryQty ? (price) => engine.defaultEntryQty!(price) : undefined,
1908
+ };
1909
+ }
1910
+
1613
1911
  /** Numeric `strategy.*` variables, read from the engine (current bar inside strategy.eachBar()). */
1614
1912
  export type StrategyNumberVariables = { readonly [K in StrategyNumberVariable]: number };
1615
1913
  /** Text `strategy.*` variables, read from the engine. */
@@ -2195,6 +2493,30 @@ function barFlag(test: (i: number, n: number) => boolean): Series {
2195
2493
  return Series.fromArray(ctxBars, Array.from({ length: n }, (_, i) => (test(i, n) ? 1 : 0)));
2196
2494
  }
2197
2495
 
2496
+ /**
2497
+ * An intrabar run reads the flag on the same bars as the run before, with at most one bar more, and the flags only
2498
+ * differ on their last bars: the values are kept between runs (the series only reads them).
2499
+ */
2500
+ const FLAG_TAIL = 3;
2501
+ const flagValues = new Map<string, number[]>();
2502
+ function namedFlag(name: string, test: (i: number, n: number) => boolean): Series {
2503
+ if (!intrabarRun) return barFlag(test);
2504
+ const n = ctxBars.bars.length;
2505
+ let values = flagValues.get(name);
2506
+ if (!values || values.length > n) {
2507
+ values = Array.from({ length: n }, (_, i) => (test(i, n) ? 1 : 0));
2508
+ flagValues.set(name, values);
2509
+ } else {
2510
+ const from = Math.max(0, Math.min(values.length, n) - FLAG_TAIL);
2511
+ values.length = n;
2512
+ for (let i = from; i < n; i++) values[i] = test(i, n) ? 1 : 0;
2513
+ }
2514
+ return Series.fromArray(ctxBars, values);
2515
+ }
2516
+
2517
+ /** Number of bars of the chart: an intrabar run ends before the chart's last bar. */
2518
+ const chartBarCount = (n: number): number => (intrabarRun ? intrabarRun.lastBarIndex + 1 : n);
2519
+
2198
2520
  const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.realtime === true;
2199
2521
 
2200
2522
  /**
@@ -2205,16 +2527,17 @@ const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.
2205
2527
  * `islastconfirmedhistory` is the last bar when it is confirmed and not realtime, else the bar before it.
2206
2528
  */
2207
2529
  export const barstate = {
2208
- get isfirst(): Series { return barFlag((i) => i === 0); },
2209
- get islast(): Series { return barFlag((i, n) => i === n - 1); },
2210
- get isconfirmed(): Series { return barFlag((i, n) => i < n - 1 || chartCtx.lastBarConfirmed === true); },
2211
- get isrealtime(): Series { return barFlag(lastRealtime); },
2212
- get ishistory(): Series { return barFlag((i, n) => !lastRealtime(i, n)); },
2213
- get isnew(): Series { return barFlag((i, n) => !lastRealtime(i, n) || chartCtx.lastBarNew !== false); },
2530
+ get isfirst(): Series { return namedFlag('isfirst', (i) => i === 0); },
2531
+ get islast(): Series { return namedFlag('islast', (i, n) => i === chartBarCount(n) - 1); },
2532
+ get isconfirmed(): Series { return namedFlag('isconfirmed', (i, n) => i < n - 1 || chartCtx.lastBarConfirmed === true); },
2533
+ get isrealtime(): Series { return namedFlag('isrealtime', lastRealtime); },
2534
+ get ishistory(): Series { return namedFlag('ishistory', (i, n) => !lastRealtime(i, n)); },
2535
+ get isnew(): Series { return namedFlag('isnew', (i, n) => !lastRealtime(i, n) || chartCtx.lastBarNew !== false); },
2214
2536
  get islastconfirmedhistory(): Series {
2215
- return barFlag((i, n) => {
2537
+ return namedFlag('islastconfirmedhistory', (i, n) => {
2216
2538
  const lastConfirmed = chartCtx.lastBarConfirmed === true && chartCtx.realtime !== true;
2217
- return i === (lastConfirmed ? n - 1 : n - 2);
2539
+ const count = chartBarCount(n);
2540
+ return i === (lastConfirmed ? count - 1 : count - 2);
2218
2541
  });
2219
2542
  },
2220
2543
  };
@@ -2658,8 +2981,11 @@ export type {
2658
2981
  } from '../strategy/index.js';
2659
2982
 
2660
2983
  /** PineScript `math.*`; `math.round_to_mintick(x)` uses `syminfo.mintick` of the chart context. */
2984
+ const { saveRandom: _saveRandom, restoreRandom: _restoreRandom, ...mathScript } = mathCore;
2985
+ void _saveRandom;
2986
+ void _restoreRandom;
2661
2987
  export const math = {
2662
- ...mathCore,
2988
+ ...mathScript,
2663
2989
  round_to_mintick: ((number: number | Series, mintick?: number) => {
2664
2990
  const tick = mintick ?? syminfo.mintick;
2665
2991
  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',