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.
- 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/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- 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/runtime/types.d.ts +6 -0
- package/dist/cjs/runtime/types.d.ts.map +1 -1
- package/dist/cjs/runtime/types.js.map +1 -1
- 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 +217 -11
- 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/index.d.ts +1 -1
- package/dist/esm/index.js +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/runtime/types.d.ts +6 -0
- package/dist/esm/runtime/types.d.ts.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 +254 -10
- 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/index.ts +1 -1
- package/src/math/index.ts +11 -0
- package/src/runtime/types.ts +6 -0
- package/src/script/index.ts +338 -10
- 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,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
|
-
|
|
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
|
-
|
|
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:
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
...
|
|
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);
|
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',
|