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.
- package/dist/cjs/drawing/registry.d.ts +37 -0
- package/dist/cjs/drawing/registry.d.ts.map +1 -1
- package/dist/cjs/drawing/registry.js +40 -0
- package/dist/cjs/drawing/registry.js.map +2 -2
- package/dist/cjs/math/index.d.ts +4 -0
- package/dist/cjs/math/index.d.ts.map +1 -1
- package/dist/cjs/math/index.js +9 -0
- package/dist/cjs/math/index.js.map +2 -2
- package/dist/cjs/script/index.d.ts +52 -1
- package/dist/cjs/script/index.d.ts.map +1 -1
- package/dist/cjs/script/index.js +222 -12
- package/dist/cjs/script/index.js.map +3 -3
- package/dist/cjs/strategy/index.d.ts +3 -0
- package/dist/cjs/strategy/index.d.ts.map +1 -1
- package/dist/cjs/strategy/index.js +1 -0
- package/dist/cjs/strategy/index.js.map +2 -2
- package/dist/esm/drawing/registry.d.ts +37 -0
- package/dist/esm/drawing/registry.d.ts.map +1 -1
- package/dist/esm/drawing/registry.js +41 -0
- package/dist/esm/drawing/registry.js.map +1 -1
- package/dist/esm/math/index.d.ts +4 -0
- package/dist/esm/math/index.d.ts.map +1 -1
- package/dist/esm/math/index.js +10 -0
- package/dist/esm/math/index.js.map +1 -1
- package/dist/esm/script/index.d.ts +52 -1
- package/dist/esm/script/index.d.ts.map +1 -1
- package/dist/esm/script/index.js +263 -13
- package/dist/esm/script/index.js.map +1 -1
- package/dist/esm/strategy/index.d.ts +3 -0
- package/dist/esm/strategy/index.d.ts.map +1 -1
- package/dist/esm/strategy/index.js +1 -0
- package/dist/esm/strategy/index.js.map +1 -1
- package/package.json +1 -1
- package/src/drawing/registry.ts +56 -0
- package/src/math/index.ts +11 -0
- package/src/script/index.ts +339 -13
- package/src/strategy/index.ts +4 -0
package/src/script/index.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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 ===
|
|
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
|
|
2209
|
-
get islast(): Series { return
|
|
2210
|
-
get isconfirmed(): Series { return
|
|
2211
|
-
get isrealtime(): Series { return
|
|
2212
|
-
get ishistory(): Series { return
|
|
2213
|
-
get isnew(): Series { return
|
|
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
|
|
2537
|
+
return namedFlag('islastconfirmedhistory', (i, n) => {
|
|
2216
2538
|
const lastConfirmed = chartCtx.lastBarConfirmed === true && chartCtx.realtime !== true;
|
|
2217
|
-
|
|
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
|
-
...
|
|
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);
|
package/src/strategy/index.ts
CHANGED
|
@@ -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',
|