oakscriptjs 0.12.3 → 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 (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 +185 -5
  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 +211 -6
  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 +295 -6
  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,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 ─────────────────────────────────────────────────────────────
@@ -1418,6 +1612,8 @@ interface HistoryBuffer {
1418
1612
  export function eachBar(fn: (c: BarContext) => number | boolean | void): Series {
1419
1613
  collector(); // same lifecycle guard (and error message) as the declaration calls
1420
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;
1421
1617
  const out: number[] = new Array(b.length).fill(NaN);
1422
1618
  const cache = new Map<Series, number[]>();
1423
1619
  const buffers = new Map<Series, HistoryBuffer>();
@@ -1427,7 +1623,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1427
1623
  get i() {
1428
1624
  return idx;
1429
1625
  },
1430
- n: b.length,
1626
+ n: lastIndex + 1,
1431
1627
  get time() {
1432
1628
  return b[idx]!.time as number;
1433
1629
  },
@@ -1472,7 +1668,7 @@ export function eachBar(fn: (c: BarContext) => number | boolean | void): Series
1472
1668
  for (idx = 0; idx < b.length; idx++) {
1473
1669
  if (pendingError && pendingError.bar === idx) throw new Error(historyError(idx, pendingError.barsBack));
1474
1670
  currentBar = idx;
1475
- setDrawingBar(idx, idx === b.length - 1, b.length - 1);
1671
+ setDrawingBar(idx, idx === lastIndex, lastIndex);
1476
1672
  const r = fn(ctx);
1477
1673
  out[idx] = typeof r === 'number' ? r : typeof r === 'boolean' ? (r ? 1 : 0) : NaN;
1478
1674
  // grow the history buffers from the largest reference of this bar
@@ -1552,6 +1748,7 @@ function strategyVariable(name: StrategyVariable): number | string {
1552
1748
  }
1553
1749
 
1554
1750
  function riskRule(rule: StrategyRiskRule): void {
1751
+ if (intrabarRun) return; // declared by the main run
1555
1752
  const engine = strategyEngine(`strategy.risk.${rule.rule}()`);
1556
1753
  if (!engine.risk) throw new Error(`strategy.risk.${rule.rule}() is not provided by the strategy engine.`);
1557
1754
  engine.risk(rule);
@@ -1598,18 +1795,103 @@ function strategyEachBar(fn: (c: BarContext) => number | boolean | void): Series
1598
1795
  const engine = strategyEngine('strategy.eachBar()');
1599
1796
  if (run.looped) throw new Error('strategy.eachBar() runs once per script run.');
1600
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;
1601
1820
  return eachBar((c) => {
1602
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
+ }
1603
1826
  run.inLoop = true;
1604
1827
  try {
1605
1828
  return fn(c);
1606
1829
  } finally {
1607
1830
  run.inLoop = false;
1831
+ run.engine = engine;
1608
1832
  engine.processClose(c.i);
1609
1833
  }
1610
1834
  });
1611
1835
  }
1612
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
+
1613
1895
  /** Numeric `strategy.*` variables, read from the engine (current bar inside strategy.eachBar()). */
1614
1896
  export type StrategyNumberVariables = { readonly [K in StrategyNumberVariable]: number };
1615
1897
  /** Text `strategy.*` variables, read from the engine. */
@@ -2195,6 +2477,9 @@ function barFlag(test: (i: number, n: number) => boolean): Series {
2195
2477
  return Series.fromArray(ctxBars, Array.from({ length: n }, (_, i) => (test(i, n) ? 1 : 0)));
2196
2478
  }
2197
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
+
2198
2483
  const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.realtime === true;
2199
2484
 
2200
2485
  /**
@@ -2206,7 +2491,7 @@ const lastRealtime = (i: number, n: number): boolean => i === n - 1 && chartCtx.
2206
2491
  */
2207
2492
  export const barstate = {
2208
2493
  get isfirst(): Series { return barFlag((i) => i === 0); },
2209
- get islast(): Series { return barFlag((i, n) => i === n - 1); },
2494
+ get islast(): Series { return barFlag((i, n) => i === chartBarCount(n) - 1); },
2210
2495
  get isconfirmed(): Series { return barFlag((i, n) => i < n - 1 || chartCtx.lastBarConfirmed === true); },
2211
2496
  get isrealtime(): Series { return barFlag(lastRealtime); },
2212
2497
  get ishistory(): Series { return barFlag((i, n) => !lastRealtime(i, n)); },
@@ -2214,7 +2499,8 @@ export const barstate = {
2214
2499
  get islastconfirmedhistory(): Series {
2215
2500
  return barFlag((i, n) => {
2216
2501
  const lastConfirmed = chartCtx.lastBarConfirmed === true && chartCtx.realtime !== true;
2217
- return i === (lastConfirmed ? n - 1 : n - 2);
2502
+ const count = chartBarCount(n);
2503
+ return i === (lastConfirmed ? count - 1 : count - 2);
2218
2504
  });
2219
2505
  },
2220
2506
  };
@@ -2658,8 +2944,11 @@ export type {
2658
2944
  } from '../strategy/index.js';
2659
2945
 
2660
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;
2661
2950
  export const math = {
2662
- ...mathCore,
2951
+ ...mathScript,
2663
2952
  round_to_mintick: ((number: number | Series, mintick?: number) => {
2664
2953
  const tick = mintick ?? syminfo.mintick;
2665
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',