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.
- 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 +185 -5
- 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 +211 -6
- 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 +295 -6
- 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 ─────────────────────────────────────────────────────────────
|
|
@@ -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:
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
...
|
|
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);
|
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',
|