@hoox-sh/pynets 0.8.0 → 0.10.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/CHANGELOG.md +32 -0
- package/README.md +17 -1
- package/dist/browser.js +63 -63
- package/dist/index.js +408 -39
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/array.ts +19 -0
- package/src/runtime/clone.ts +26 -0
- package/src/runtime/compile/collections.ts +13 -0
- package/src/runtime/compile/emit.ts +19 -1
- package/src/runtime/compile/emit_call.ts +15 -0
- package/src/runtime/compile/types.ts +2 -0
- package/src/runtime/interpret.ts +293 -36
- package/src/runtime/map.ts +25 -0
- package/src/runtime/matrix.ts +25 -0
- package/src/runtime/series.ts +6 -0
- package/src/runtime/strategy.ts +32 -0
- package/src/runtime/ta.ts +28 -0
- package/src/runtime/udt.ts +22 -0
package/src/runtime/array.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { UdtInstance } from "./udt.ts";
|
|
11
|
+
import { cloneValue } from "./clone.ts";
|
|
11
12
|
|
|
12
13
|
/** Numeric/na cell. Arrays may also store UDT instances, strings, and bools. */
|
|
13
14
|
export type Cell = number | null;
|
|
@@ -245,6 +246,24 @@ export class PineArray {
|
|
|
245
246
|
return out;
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
/** Deep clone (realtime tick snapshots). */
|
|
250
|
+
clone(): PineArray {
|
|
251
|
+
const out = new PineArray();
|
|
252
|
+
out.restoreContents(this.snapshotContents());
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Deep-cloned slots for realtime tick rollback. */
|
|
257
|
+
snapshotContents(): unknown[] {
|
|
258
|
+
return this.cells.map(cloneValue);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Replace slots with clones of `snap` — same instance, aliases keep identity. */
|
|
262
|
+
restoreContents(snap: unknown[]): void {
|
|
263
|
+
this.cells.length = 0;
|
|
264
|
+
for (const c of snap) this.cells.push(cloneValue(c));
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
reverse(): void {
|
|
249
268
|
this.cells.reverse();
|
|
250
269
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Deep-clone runtime values for realtime tick snapshots (Python
|
|
6
|
+
* `_snapshot_realtime_scope` / `_snapshot_ta_state` analog). Containers
|
|
7
|
+
* duck-type `clone()` so this module stays import-free.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Deep clone: primitives, arrays, Map/Set, containers (via `clone()`), plain objects. */
|
|
11
|
+
export function cloneValue(v: unknown): unknown {
|
|
12
|
+
if (v == null) return v;
|
|
13
|
+
if (typeof v !== "object") return v;
|
|
14
|
+
if (Array.isArray(v)) return v.map(cloneValue);
|
|
15
|
+
if (v instanceof Map) {
|
|
16
|
+
const out = new Map<unknown, unknown>();
|
|
17
|
+
for (const [k, x] of v) out.set(k, cloneValue(x));
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
if (v instanceof Set) return new Set(v);
|
|
21
|
+
const clone = (v as { clone?: () => unknown }).clone;
|
|
22
|
+
if (typeof clone === "function") return clone.call(v);
|
|
23
|
+
const out: Record<string, unknown> = {};
|
|
24
|
+
for (const [k, x] of Object.entries(v)) out[k] = cloneValue(x);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
@@ -86,6 +86,19 @@ export const compileArray = {
|
|
|
86
86
|
get(id: unknown, index?: unknown): unknown {
|
|
87
87
|
return asArray(id)?.get(n(index, 0)) ?? null;
|
|
88
88
|
},
|
|
89
|
+
/**
|
|
90
|
+
* Python `visit_Subscript` list path: `arr[0]` is the LAST slot
|
|
91
|
+
* (`value[-(offset + 1)]`, newest-first like series offsets). `na` /
|
|
92
|
+
* negative / OOB offsets soft-fail to `na`.
|
|
93
|
+
*/
|
|
94
|
+
sub(id: unknown, offset?: unknown): unknown {
|
|
95
|
+
const a = asArray(id);
|
|
96
|
+
const off = cell(offset);
|
|
97
|
+
if (a == null || off == null || off < 0) return null;
|
|
98
|
+
const size = a.size();
|
|
99
|
+
const i = size - 1 - Math.trunc(off);
|
|
100
|
+
return i < 0 || i >= size ? null : a.get(i);
|
|
101
|
+
},
|
|
89
102
|
set(id: unknown, index?: unknown, value?: unknown): unknown {
|
|
90
103
|
asArray(id)?.set(n(index, 0), store(value));
|
|
91
104
|
return id ?? null;
|
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
UnaryOp,
|
|
36
36
|
While,
|
|
37
37
|
} from "../../ast/nodes.ts";
|
|
38
|
-
import { emitCall, TA_BARE_ATTRS } from "./emit_call.ts";
|
|
38
|
+
import { emitCall, isArrayReturningCall, TA_BARE_ATTRS } from "./emit_call.ts";
|
|
39
39
|
import { emitFunctionDef } from "./emit_udf.ts";
|
|
40
40
|
import type { EmitCtx, VisitFn } from "./types.ts";
|
|
41
41
|
|
|
@@ -134,6 +134,7 @@ interface State {
|
|
|
134
134
|
export function newEmitCtx(): EmitCtx {
|
|
135
135
|
return {
|
|
136
136
|
arrays: new Set(),
|
|
137
|
+
arrayNames: new Set(),
|
|
137
138
|
plots: [],
|
|
138
139
|
varNames: new Set(),
|
|
139
140
|
functions: [],
|
|
@@ -980,6 +981,11 @@ function emitAttribute(state: State, node: Attribute): string {
|
|
|
980
981
|
function emitSubscript(state: State, node: Subscript): string {
|
|
981
982
|
const slice = node.slice != null ? emitExpr(state, node.slice) : "0";
|
|
982
983
|
const arr = histArray(state, node.value);
|
|
984
|
+
// Array-valued names index the current PineArray (Python list subscript:
|
|
985
|
+
// newest-first, `value[-(offset + 1)]`), not series history.
|
|
986
|
+
if (node.value.kind === "Name" && state.ctx.arrayNames.has((node.value as Name).id)) {
|
|
987
|
+
return `__h.array.sub(${arr}[__bar_idx], ${slice})`;
|
|
988
|
+
}
|
|
983
989
|
return `__h.hist(${arr}, __bar_idx, ${slice})`;
|
|
984
990
|
}
|
|
985
991
|
|
|
@@ -1082,13 +1088,25 @@ function emitImport(state: State, node: Import): string {
|
|
|
1082
1088
|
function emitAssign(state: State, node: Assign): string {
|
|
1083
1089
|
const rhs = node.value != null ? emitExpr(state, node.value) : "null";
|
|
1084
1090
|
const isVar = isVarMode(node.mode);
|
|
1091
|
+
trackArrayName(state, node.target, node.value);
|
|
1085
1092
|
return emitStore(state, node.target, rhs, isVar);
|
|
1086
1093
|
}
|
|
1087
1094
|
|
|
1088
1095
|
function emitReAssign(state: State, node: ReAssign): string {
|
|
1096
|
+
trackArrayName(state, node.target, node.value);
|
|
1089
1097
|
return emitStore(state, node.target, emitExpr(state, node.value), false);
|
|
1090
1098
|
}
|
|
1091
1099
|
|
|
1100
|
+
/** Names bound to PineArray results get list-subscript lowering (`g[0]`). */
|
|
1101
|
+
function trackArrayName(state: State, target: expr, value: expr | null | undefined): void {
|
|
1102
|
+
if (target.kind !== "Name") return;
|
|
1103
|
+
if (value != null && value.kind === "Call" && isArrayReturningCall((value as Call).func)) {
|
|
1104
|
+
state.ctx.arrayNames.add(target.id);
|
|
1105
|
+
} else {
|
|
1106
|
+
state.ctx.arrayNames.delete(target.id);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1092
1110
|
function emitAugAssign(state: State, node: AugAssign): string {
|
|
1093
1111
|
// Attribute targets store the raw rhs — the old field is discarded
|
|
1094
1112
|
// (quirk preserved from Python visit_AugAssign and interpret evalAugAssign).
|
|
@@ -560,6 +560,21 @@ function unwrapSpecialize(node: expr): expr {
|
|
|
560
560
|
return node;
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Calls whose result binds a PineArray value, so a later `name[i]` subscript
|
|
565
|
+
* lowers to `__h.array.sub` (Python `visit_Subscript` list path) instead of
|
|
566
|
+
* series history. Python `mode="compile"` does not support list subscripts
|
|
567
|
+
* (returns na); PyneTS keeps compile aligned with interpret instead.
|
|
568
|
+
*/
|
|
569
|
+
export function isArrayReturningCall(func: expr): boolean {
|
|
570
|
+
const { name } = resolveCallee(func);
|
|
571
|
+
if (name === "ta_gamma_levels" || name === "gamma_levels") return true;
|
|
572
|
+
if (name === "array_from" || name === "array_copy" || name === "array_concat" || name === "array_slice") {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return name.startsWith("array_new");
|
|
576
|
+
}
|
|
577
|
+
|
|
563
578
|
function collectArgs(node: Call, visit: VisitFn): CallArgs {
|
|
564
579
|
const pos: string[] = [];
|
|
565
580
|
const kw: Record<string, string> = {};
|
|
@@ -51,6 +51,8 @@ export class CompileIneligibleError extends CompileError {
|
|
|
51
51
|
/** Visitor state while lowering a Script to JS. */
|
|
52
52
|
export interface EmitCtx {
|
|
53
53
|
arrays: Set<string>;
|
|
54
|
+
/** Script-scope names bound to PineArray values (list subscript `g[0]`). */
|
|
55
|
+
arrayNames: Set<string>;
|
|
54
56
|
plots: CompilePlotMeta[];
|
|
55
57
|
varNames: Set<string>;
|
|
56
58
|
functions: string[];
|
package/src/runtime/interpret.ts
CHANGED
|
@@ -218,6 +218,14 @@ export interface RuntimeOptions {
|
|
|
218
218
|
mode?: RuntimeMode;
|
|
219
219
|
/** Wall-clock budget in seconds. Interpret only; checked every 32 bars. */
|
|
220
220
|
timeout_seconds?: number | null;
|
|
221
|
+
/** Visit the last bar as a forming realtime bar (`barstate.isrealtime`). */
|
|
222
|
+
realtimeLastBar?: boolean;
|
|
223
|
+
/** Visits per realtime bar (Python `realtime_ticks`; >1 implies a last-bar window). */
|
|
224
|
+
realtimeTicks?: number;
|
|
225
|
+
/** Size of the trailing realtime window (Python `realtime_bars`). */
|
|
226
|
+
realtimeBars?: number;
|
|
227
|
+
/** First bar index of the realtime window (Python `realtime_from_bar`). */
|
|
228
|
+
realtimeFromBar?: number | null;
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
export interface RuntimeFill {
|
|
@@ -362,6 +370,18 @@ interface Env {
|
|
|
362
370
|
maxBarsBackDecls: Array<{ var: Value; num: Value }>;
|
|
363
371
|
/** Python `_alert_fire_bars` keys for `alert` / `alertcondition` freq gating. */
|
|
364
372
|
alertFireBars: Map<string, number>;
|
|
373
|
+
/** First bar of the realtime window; null = purely historical run. */
|
|
374
|
+
realtimeFirst: number | null;
|
|
375
|
+
/** Visits per realtime bar (Python `realtime_ticks`). */
|
|
376
|
+
realtimeTicks: number;
|
|
377
|
+
/** True when the current bar is inside the realtime window. */
|
|
378
|
+
barRealtime: boolean;
|
|
379
|
+
/** Current tick index within the bar (0 = first visit). */
|
|
380
|
+
tickIndex: number;
|
|
381
|
+
/** Total visits for the current bar. */
|
|
382
|
+
tickCount: number;
|
|
383
|
+
/** `varip`-declared names — persist across realtime ticks (reference varip). */
|
|
384
|
+
varipNames: Set<string>;
|
|
365
385
|
}
|
|
366
386
|
|
|
367
387
|
export class Runtime {
|
|
@@ -413,6 +433,12 @@ export class Runtime {
|
|
|
413
433
|
const extraOpts = extra && !isPlainInputs(extra) ? extra : undefined;
|
|
414
434
|
const mode = extraOpts?.mode ?? this.mode;
|
|
415
435
|
const inputs = mergeInputs(this.inputs, extra);
|
|
436
|
+
const rt = resolveRealtimeWindow(ohlcv.length, {
|
|
437
|
+
realtimeLastBar: extraOpts?.realtimeLastBar,
|
|
438
|
+
realtimeTicks: extraOpts?.realtimeTicks,
|
|
439
|
+
realtimeBars: extraOpts?.realtimeBars,
|
|
440
|
+
realtimeFromBar: extraOpts?.realtimeFromBar,
|
|
441
|
+
});
|
|
416
442
|
const host = {
|
|
417
443
|
symbol: this.symbol,
|
|
418
444
|
inputs,
|
|
@@ -420,6 +446,8 @@ export class Runtime {
|
|
|
420
446
|
timeframe: extraOpts?.timeframe ?? this.timeframe,
|
|
421
447
|
libraries: extraOpts?.libraries != null ? resolveLibraries(extraOpts.libraries) : this.libraries,
|
|
422
448
|
timeout_seconds: extraOpts?.timeout_seconds ?? this.timeout_seconds,
|
|
449
|
+
realtime_first: rt.first,
|
|
450
|
+
realtime_ticks: rt.ticks,
|
|
423
451
|
};
|
|
424
452
|
if (mode === "compile") {
|
|
425
453
|
return runCompiled(source, ohlcv, host);
|
|
@@ -494,7 +522,7 @@ export class RuntimeStream {
|
|
|
494
522
|
if (this.closed) return undefined;
|
|
495
523
|
this.bars.push(bar);
|
|
496
524
|
try {
|
|
497
|
-
const out = this.runtime.run(this.source, this.bars);
|
|
525
|
+
const out = this.runtime.run(this.source, this.bars, { realtimeLastBar: true });
|
|
498
526
|
for (const h of this.barHandlers) h(out);
|
|
499
527
|
return out;
|
|
500
528
|
} catch (err) {
|
|
@@ -504,9 +532,35 @@ export class RuntimeStream {
|
|
|
504
532
|
}
|
|
505
533
|
}
|
|
506
534
|
|
|
535
|
+
/** Number of committed bars in the stream. */
|
|
536
|
+
get barCount(): number {
|
|
537
|
+
return this.bars.length;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Re-tick the forming last bar with revised OHLCV. The bar count does not
|
|
542
|
+
* advance; the script is re-evaluated from bar 0, so `once`, TA state, and
|
|
543
|
+
* broker fills naturally roll back to the revised history.
|
|
544
|
+
*/
|
|
545
|
+
updateLastBar(bar: OHLCVBar): RuntimeResult {
|
|
546
|
+
if (this.closed) throw new Error("stream is closed");
|
|
547
|
+
if (this.bars.length === 0) throw new Error("updateLastBar requires at least one bar");
|
|
548
|
+
this.bars[this.bars.length - 1] = bar;
|
|
549
|
+
const out = this.runtime.run(this.source, this.bars, { realtimeLastBar: true });
|
|
550
|
+
for (const h of this.barHandlers) h(out);
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Append several bars, returning the final result. */
|
|
555
|
+
appendBars(bars: OHLCVBar[]): RuntimeResult | undefined {
|
|
556
|
+
let out: RuntimeResult | undefined;
|
|
557
|
+
for (const bar of bars) out = this.push(bar);
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
|
|
507
561
|
close(): RuntimeResult {
|
|
508
562
|
this.closed = true;
|
|
509
|
-
const out = this.runtime.run(this.source, this.bars);
|
|
563
|
+
const out = this.runtime.run(this.source, this.bars, { realtimeLastBar: false });
|
|
510
564
|
for (const h of this.endHandlers) h(out);
|
|
511
565
|
return out;
|
|
512
566
|
}
|
|
@@ -518,6 +572,126 @@ export function interpret(source: string, ohlcv: OHLCVBar[]): { plots: Array<num
|
|
|
518
572
|
return { plots: out.plots };
|
|
519
573
|
}
|
|
520
574
|
|
|
575
|
+
/** Python `_snapshot_realtime_scope` analog — one intermediate realtime tick. */
|
|
576
|
+
interface TickSnapshot {
|
|
577
|
+
ctx: Record<string, Value>;
|
|
578
|
+
containers: Array<[string, unknown]>;
|
|
579
|
+
varInited: Set<string>;
|
|
580
|
+
plotsLens: Map<string, number>;
|
|
581
|
+
plotMetaLen: number;
|
|
582
|
+
ta: Array<[string, Map<string, unknown>]>;
|
|
583
|
+
book: Record<string, unknown>;
|
|
584
|
+
udfSites: Array<[string, Record<string, Value>, Set<string>, Map<string, number>]>;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isSnapshotContainer(v: unknown): boolean {
|
|
588
|
+
return (
|
|
589
|
+
v !== null &&
|
|
590
|
+
typeof v === "object" &&
|
|
591
|
+
typeof (v as { snapshotContents?: unknown }).snapshotContents === "function"
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function isRestoreContainer(v: unknown): v is { restoreContents(snap: unknown): void } {
|
|
596
|
+
return (
|
|
597
|
+
v !== null &&
|
|
598
|
+
typeof v === "object" &&
|
|
599
|
+
typeof (v as { restoreContents?: unknown }).restoreContents === "function"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Undo state for one intermediate realtime tick (Python `_run_batch` tick loop). */
|
|
604
|
+
function snapshotTickState(env: Env): TickSnapshot {
|
|
605
|
+
const containers: Array<[string, unknown]> = [];
|
|
606
|
+
for (const [id, v] of Object.entries(env.ctx)) {
|
|
607
|
+
if (isSnapshotContainer(v)) {
|
|
608
|
+
containers.push([id, (v as { snapshotContents(): unknown }).snapshotContents()]);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const plotsLens = new Map<string, number>();
|
|
612
|
+
for (const [key, arr] of Object.entries(env.plots)) plotsLens.set(key, arr.length);
|
|
613
|
+
const udfSites: Array<[string, Record<string, Value>, Set<string>, Map<string, number>]> = [];
|
|
614
|
+
for (const [key, f] of env.udfSites) {
|
|
615
|
+
const seriesLens = new Map<string, number>();
|
|
616
|
+
for (const [name, s] of f.series) seriesLens.set(name, s.length);
|
|
617
|
+
udfSites.push([key, { ...f.ctx }, new Set(f.varInited), seriesLens]);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
ctx: { ...env.ctx },
|
|
621
|
+
containers,
|
|
622
|
+
varInited: new Set(env.varInited),
|
|
623
|
+
plotsLens,
|
|
624
|
+
plotMetaLen: env.plot_meta.length,
|
|
625
|
+
ta: env.ta.snapshot(),
|
|
626
|
+
book: env.book.snapshot(),
|
|
627
|
+
udfSites,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Roll back one intermediate realtime tick: var scope (varip persists, Python
|
|
633
|
+
* `_restore_realtime_scope`), container contents in place, UDF-site var state,
|
|
634
|
+
* plot cells appended this tick (Python `_discard_realtime_plot_tick`), TA
|
|
635
|
+
* windows (Python `_restore_ta_state`), and broker state. History pushes are
|
|
636
|
+
* intentionally left alone — the start-of-bar commit already ran.
|
|
637
|
+
*/
|
|
638
|
+
function restoreTickState(env: Env, snap: TickSnapshot): void {
|
|
639
|
+
const varips = env.varipNames;
|
|
640
|
+
for (const key of Object.keys(env.ctx)) {
|
|
641
|
+
if (!(key in snap.ctx) && !varips.has(key)) delete env.ctx[key];
|
|
642
|
+
}
|
|
643
|
+
for (const [key, value] of Object.entries(snap.ctx)) {
|
|
644
|
+
if (varips.has(key)) continue;
|
|
645
|
+
env.ctx[key] = value;
|
|
646
|
+
}
|
|
647
|
+
for (const [id, contents] of snap.containers) {
|
|
648
|
+
if (varips.has(id)) continue;
|
|
649
|
+
const cur = env.ctx[id];
|
|
650
|
+
if (isRestoreContainer(cur)) {
|
|
651
|
+
(cur as unknown as { restoreContents(snap: unknown): void }).restoreContents(contents);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const restoredInited = new Set(snap.varInited);
|
|
655
|
+
for (const id of varips) {
|
|
656
|
+
if (env.varInited.has(id)) restoredInited.add(id);
|
|
657
|
+
}
|
|
658
|
+
env.varInited = restoredInited;
|
|
659
|
+
for (const key of Object.keys(env.plots)) {
|
|
660
|
+
const len = snap.plotsLens.get(key);
|
|
661
|
+
if (len === undefined) delete env.plots[key];
|
|
662
|
+
else if (env.plots[key]!.length > len) env.plots[key]!.length = len;
|
|
663
|
+
}
|
|
664
|
+
env.plot_meta.length = snap.plotMetaLen;
|
|
665
|
+
env.ta.restore(snap.ta);
|
|
666
|
+
env.book.restore(snap.book);
|
|
667
|
+
const siteKeys = new Set(snap.udfSites.map(([key]) => key));
|
|
668
|
+
for (const key of [...env.udfSites.keys()]) {
|
|
669
|
+
if (!siteKeys.has(key)) env.udfSites.delete(key);
|
|
670
|
+
}
|
|
671
|
+
for (const [key, ctx, varInited, seriesLens] of snap.udfSites) {
|
|
672
|
+
const f = env.udfSites.get(key);
|
|
673
|
+
if (f == null) continue;
|
|
674
|
+
for (const k of Object.keys(f.ctx)) {
|
|
675
|
+
if (!(k in ctx) && !varips.has(k)) delete f.ctx[k];
|
|
676
|
+
}
|
|
677
|
+
for (const [k, v] of Object.entries(ctx)) {
|
|
678
|
+
if (varips.has(k)) continue;
|
|
679
|
+
f.ctx[k] = v;
|
|
680
|
+
}
|
|
681
|
+
// Roll back per-tick series pushes so the next tick's carry reads the
|
|
682
|
+
// pre-tick current (Python leaves history alone; frames re-carry per call).
|
|
683
|
+
for (const [name, len] of seriesLens) {
|
|
684
|
+
const s = f.series.get(name);
|
|
685
|
+
if (s != null && s.length > len) s.truncate(len);
|
|
686
|
+
}
|
|
687
|
+
const restored = new Set(varInited);
|
|
688
|
+
for (const id of varips) {
|
|
689
|
+
if (f.varInited.has(id)) restored.add(id);
|
|
690
|
+
}
|
|
691
|
+
f.varInited = restored;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
521
695
|
export function interpretTree(
|
|
522
696
|
tree: AST,
|
|
523
697
|
ohlcv: OHLCVBar[],
|
|
@@ -528,6 +702,8 @@ export function interpretTree(
|
|
|
528
702
|
timeframe?: string | null;
|
|
529
703
|
libraries?: LibraryRegistry;
|
|
530
704
|
timeout_seconds?: number | null;
|
|
705
|
+
realtime_first?: number | null;
|
|
706
|
+
realtime_ticks?: number;
|
|
531
707
|
},
|
|
532
708
|
): RuntimeResult {
|
|
533
709
|
const scriptNode = tree.kind === "Script" ? (tree as Script) : null;
|
|
@@ -566,6 +742,12 @@ export function interpretTree(
|
|
|
566
742
|
onceFired: new WeakSet(),
|
|
567
743
|
maxBarsBackDecls: [],
|
|
568
744
|
alertFireBars: new Map(),
|
|
745
|
+
realtimeFirst: host?.realtime_first ?? null,
|
|
746
|
+
realtimeTicks: Math.max(1, host?.realtime_ticks ?? 1),
|
|
747
|
+
barRealtime: false,
|
|
748
|
+
tickIndex: 0,
|
|
749
|
+
tickCount: 1,
|
|
750
|
+
varipNames: new Set(),
|
|
569
751
|
};
|
|
570
752
|
|
|
571
753
|
const n = ohlcv.length;
|
|
@@ -575,8 +757,9 @@ export function interpretTree(
|
|
|
575
757
|
const timeoutSec = host?.timeout_seconds;
|
|
576
758
|
const deadline =
|
|
577
759
|
timeoutSec != null && Number.isFinite(timeoutSec) ? performance.now() + timeoutSec * 1000 : null;
|
|
760
|
+
let aborted = false;
|
|
578
761
|
try {
|
|
579
|
-
for (let i = 0; i < n; i++) {
|
|
762
|
+
for (let i = 0; i < n && !aborted; i++) {
|
|
580
763
|
if (deadline != null && (i & 31) === 0 && performance.now() > deadline) {
|
|
581
764
|
timedOut = true;
|
|
582
765
|
barsDone = i;
|
|
@@ -596,31 +779,44 @@ export function interpretTree(
|
|
|
596
779
|
num(env.ctx.close) ?? 0,
|
|
597
780
|
);
|
|
598
781
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
782
|
+
// Python `_run_batch`: bars in the realtime window are visited once per
|
|
783
|
+
// tick; intermediate ticks roll back var scope, TA windows, the broker,
|
|
784
|
+
// and plot cells (varip persists).
|
|
785
|
+
const barRt = env.realtimeFirst != null && i >= env.realtimeFirst;
|
|
786
|
+
const ticks = barRt ? env.realtimeTicks : 1;
|
|
787
|
+
env.barRealtime = barRt;
|
|
788
|
+
env.tickCount = ticks;
|
|
789
|
+
for (let tick = 0; tick < ticks; tick++) {
|
|
790
|
+
env.tickIndex = tick;
|
|
791
|
+
const snap = barRt && tick < ticks - 1 ? snapshotTickState(env) : null;
|
|
792
|
+
try {
|
|
793
|
+
for (const stmt of body) {
|
|
794
|
+
const decl = captureDecl(stmt);
|
|
795
|
+
if (decl) {
|
|
796
|
+
if (script_name == null) script_name = decl.name;
|
|
797
|
+
if (decl.kind === "strategy") {
|
|
798
|
+
script_type = "strategy";
|
|
799
|
+
applyStrategyDecl(stmt, env);
|
|
800
|
+
} else if (decl.kind === "library") {
|
|
801
|
+
script_type = "library";
|
|
802
|
+
applyDrawingDecl(stmt, env);
|
|
803
|
+
} else {
|
|
804
|
+
applyDrawingDecl(stmt, env);
|
|
805
|
+
}
|
|
806
|
+
continue;
|
|
612
807
|
}
|
|
613
|
-
|
|
808
|
+
execStmt(stmt, env);
|
|
614
809
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
810
|
+
if (i === 0 && script_type === "library" && script_name) {
|
|
811
|
+
finalizeLibrary(env, script_name);
|
|
812
|
+
}
|
|
813
|
+
} catch (err) {
|
|
814
|
+
if (err === LOOP_BREAK || err === LOOP_CONTINUE) break;
|
|
815
|
+
runError = { error: formatRunError(err), error_kind: "runtime" };
|
|
816
|
+
aborted = true;
|
|
817
|
+
break;
|
|
619
818
|
}
|
|
620
|
-
|
|
621
|
-
if (err === LOOP_BREAK || err === LOOP_CONTINUE) continue;
|
|
622
|
-
runError = { error: formatRunError(err), error_kind: "runtime" };
|
|
623
|
-
break;
|
|
819
|
+
if (snap) restoreTickState(env, snap);
|
|
624
820
|
}
|
|
625
821
|
}
|
|
626
822
|
} catch (err) {
|
|
@@ -898,7 +1094,8 @@ function uniquifyPlotKey(env: Env, node: object, title: string): string {
|
|
|
898
1094
|
}
|
|
899
1095
|
|
|
900
1096
|
function isVarMode(stmt: Assign): boolean {
|
|
901
|
-
|
|
1097
|
+
// `varip` matches `var` on historical bars: init on first execution, persist.
|
|
1098
|
+
return stmt.mode?.kind === "Var" || stmt.mode?.kind === "VarIp";
|
|
902
1099
|
}
|
|
903
1100
|
|
|
904
1101
|
function isTuple(v: Value): v is TupleVal {
|
|
@@ -1022,6 +1219,16 @@ function evalAssign(stmt: Assign, env: Env): Value {
|
|
|
1022
1219
|
}
|
|
1023
1220
|
if (stmt.target.kind !== "Name") return NA;
|
|
1024
1221
|
const id = stmt.target.id;
|
|
1222
|
+
const isVarip = stmt.mode?.kind === "VarIp";
|
|
1223
|
+
if (isVarip) env.varipNames.add(id);
|
|
1224
|
+
if (isVarip && env.barRealtime) {
|
|
1225
|
+
// Reference varip (Python `ast.VarIp` + `barstate.isrealtime`): re-run the
|
|
1226
|
+
// initializer on every realtime tick so the value can track intrabar state.
|
|
1227
|
+
const value = evalExpr(stmt.value, env);
|
|
1228
|
+
bindName(env, id, value);
|
|
1229
|
+
inited.add(id);
|
|
1230
|
+
return value;
|
|
1231
|
+
}
|
|
1025
1232
|
// var: init on first execution (bar_index==0, or first time the name is missing).
|
|
1026
1233
|
if (isVarMode(stmt) && (inited.has(id) || (env.barIndex !== 0 && ctxGet(env, id) !== undefined))) {
|
|
1027
1234
|
inited.add(id);
|
|
@@ -1276,6 +1483,14 @@ function evalExpr(node: expr | undefined, env: Env): Value {
|
|
|
1276
1483
|
if (node.value.kind === "Name") {
|
|
1277
1484
|
const s = seriesGet(env, node.value.id);
|
|
1278
1485
|
if (s) return s.get(offset);
|
|
1486
|
+
// Python `visit_Subscript` list path: array-valued names index
|
|
1487
|
+
// newest-first (`value[-(offset + 1)]`); OOB soft-fails to na.
|
|
1488
|
+
const v = ctxGet(env, node.value.id);
|
|
1489
|
+
if (v instanceof PineArray) {
|
|
1490
|
+
const size = v.size();
|
|
1491
|
+
const i = size - 1 - offset;
|
|
1492
|
+
return i < 0 || i >= size ? NA : (v.get(i) as Value);
|
|
1493
|
+
}
|
|
1279
1494
|
}
|
|
1280
1495
|
return NA;
|
|
1281
1496
|
}
|
|
@@ -1319,7 +1534,9 @@ function evalExpr(node: expr | undefined, env: Env): Value {
|
|
|
1319
1534
|
const t = onceNode.test == null ? 1 : unwrap(evalExpr(onceNode.test, env));
|
|
1320
1535
|
if (!isTruthy(t)) return NA;
|
|
1321
1536
|
for (const s of onceNode.body) execStmt(s, env);
|
|
1322
|
-
|
|
1537
|
+
// Python commits the fired flag only on a confirmed bar; unconfirmed
|
|
1538
|
+
// realtime ticks may re-run the body (rollback semantics).
|
|
1539
|
+
if (barIsConfirmed(env)) env.onceFired.add(onceNode);
|
|
1323
1540
|
return NA;
|
|
1324
1541
|
}
|
|
1325
1542
|
case "BoolOp":
|
|
@@ -1652,13 +1869,14 @@ function evalAttribute(node: Attribute, env: Env): Value {
|
|
|
1652
1869
|
}
|
|
1653
1870
|
if (node.value.id === "barstate") {
|
|
1654
1871
|
const last = env.barIndex === env.barCount - 1;
|
|
1872
|
+
const rt = env.barRealtime;
|
|
1655
1873
|
if (node.attr === "isfirst") return env.barIndex === 0 ? 1 : 0;
|
|
1656
1874
|
if (node.attr === "islast") return last ? 1 : 0;
|
|
1657
|
-
if (node.attr === "ishistory") return
|
|
1658
|
-
if (node.attr === "isconfirmed") return 1;
|
|
1659
|
-
if (node.attr === "isnew") return 1;
|
|
1660
|
-
if (node.attr === "isrealtime") return 0;
|
|
1661
|
-
if (node.attr === "islastconfirmedhistory") return last ? 1 : 0;
|
|
1875
|
+
if (node.attr === "ishistory") return rt ? 0 : 1;
|
|
1876
|
+
if (node.attr === "isconfirmed") return barIsConfirmed(env) ? 1 : 0;
|
|
1877
|
+
if (node.attr === "isnew") return rt && env.tickIndex > 0 ? 0 : 1;
|
|
1878
|
+
if (node.attr === "isrealtime") return rt ? 1 : 0;
|
|
1879
|
+
if (node.attr === "islastconfirmedhistory") return rt ? 0 : last ? 1 : 0;
|
|
1662
1880
|
}
|
|
1663
1881
|
if (node.value.id === "session") {
|
|
1664
1882
|
if (node.attr === "regular") return "regular";
|
|
@@ -2463,6 +2681,39 @@ function formatRunError(err: unknown): string {
|
|
|
2463
2681
|
return "runtime error";
|
|
2464
2682
|
}
|
|
2465
2683
|
|
|
2684
|
+
/**
|
|
2685
|
+
* Python `_resolve_realtime_first`: realtime window start (inclusive) + tick
|
|
2686
|
+
* count. Precedence: `realtimeFromBar` > `realtimeBars` > last-bar flags
|
|
2687
|
+
* (`realtimeLastBar` or `realtimeTicks > 1`). `first = null` = purely
|
|
2688
|
+
* historical run.
|
|
2689
|
+
*/
|
|
2690
|
+
export function resolveRealtimeWindow(nBars: number, opts: {
|
|
2691
|
+
realtimeLastBar?: boolean;
|
|
2692
|
+
realtimeTicks?: number;
|
|
2693
|
+
realtimeBars?: number;
|
|
2694
|
+
realtimeFromBar?: number | null;
|
|
2695
|
+
}): { first: number | null; ticks: number } {
|
|
2696
|
+
const intOr = (v: number | undefined | null, fallback: number): number => {
|
|
2697
|
+
if (v == null || !Number.isFinite(v)) return fallback;
|
|
2698
|
+
return Math.trunc(v);
|
|
2699
|
+
};
|
|
2700
|
+
let ticks = intOr(opts.realtimeTicks, 1);
|
|
2701
|
+
if (ticks < 1) ticks = 1;
|
|
2702
|
+
let rtBars = intOr(opts.realtimeBars, 0);
|
|
2703
|
+
if (rtBars < 0) rtBars = 0;
|
|
2704
|
+
if (opts.realtimeFromBar != null) {
|
|
2705
|
+
let first = intOr(opts.realtimeFromBar, 0);
|
|
2706
|
+
if (first < 0) first = 0;
|
|
2707
|
+
if (first >= nBars) return { first: null, ticks };
|
|
2708
|
+
return { first, ticks };
|
|
2709
|
+
}
|
|
2710
|
+
if (rtBars > 0) return { first: nBars > rtBars ? nBars - rtBars : 0, ticks };
|
|
2711
|
+
if (opts.realtimeLastBar || ticks > 1) {
|
|
2712
|
+
return { first: nBars > 0 ? nBars - 1 : null, ticks };
|
|
2713
|
+
}
|
|
2714
|
+
return { first: null, ticks };
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2466
2717
|
function isPlainInputs(extra: RuntimeOptions | InputOverrides): extra is InputOverrides {
|
|
2467
2718
|
return (
|
|
2468
2719
|
!("inputs" in extra) &&
|
|
@@ -2470,7 +2721,11 @@ function isPlainInputs(extra: RuntimeOptions | InputOverrides): extra is InputOv
|
|
|
2470
2721
|
!("timeframe" in extra) &&
|
|
2471
2722
|
!("libraries" in extra) &&
|
|
2472
2723
|
!("mode" in extra) &&
|
|
2473
|
-
!("timeout_seconds" in extra)
|
|
2724
|
+
!("timeout_seconds" in extra) &&
|
|
2725
|
+
!("realtimeLastBar" in extra) &&
|
|
2726
|
+
!("realtimeTicks" in extra) &&
|
|
2727
|
+
!("realtimeBars" in extra) &&
|
|
2728
|
+
!("realtimeFromBar" in extra)
|
|
2474
2729
|
);
|
|
2475
2730
|
}
|
|
2476
2731
|
|
|
@@ -2623,6 +2878,8 @@ function runAuto(
|
|
|
2623
2878
|
timeframe?: string | null;
|
|
2624
2879
|
libraries?: LibraryRegistry;
|
|
2625
2880
|
timeout_seconds?: number | null;
|
|
2881
|
+
realtime_first?: number | null;
|
|
2882
|
+
realtime_ticks?: number;
|
|
2626
2883
|
},
|
|
2627
2884
|
): RuntimeResult {
|
|
2628
2885
|
const elig = compileEligible(source);
|
|
@@ -2700,9 +2957,9 @@ function normalizeAlertFreq(freq: unknown): string {
|
|
|
2700
2957
|
return ALERT_FREQ_ALIASES[s] ?? (s ? s : FREQ_ONCE_PER_BAR);
|
|
2701
2958
|
}
|
|
2702
2959
|
|
|
2703
|
-
/** Python `_bar_is_confirmed
|
|
2704
|
-
function barIsConfirmed(
|
|
2705
|
-
return
|
|
2960
|
+
/** Python `_bar_is_confirmed`: false only on intermediate realtime ticks. */
|
|
2961
|
+
function barIsConfirmed(env: Env): boolean {
|
|
2962
|
+
return !env.barRealtime || env.tickIndex === env.tickCount - 1;
|
|
2706
2963
|
}
|
|
2707
2964
|
|
|
2708
2965
|
/** Python `_should_fire_alert`. */
|
package/src/runtime/map.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Tiny Pine `map.*` value. Keys are string or finite number; `null` is `na`.
|
|
6
6
|
* Backed by `Map` (never a plain object) so user keys cannot pollute prototypes.
|
|
7
7
|
*/
|
|
8
|
+
|
|
9
|
+
import { cloneValue } from "./clone.ts";
|
|
10
|
+
|
|
8
11
|
export type Cell = number | null;
|
|
9
12
|
export type MapKey = string | number;
|
|
10
13
|
|
|
@@ -76,6 +79,28 @@ export class PineMap {
|
|
|
76
79
|
return out;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/** Deep clone (realtime tick snapshots). */
|
|
83
|
+
clone(): PineMap {
|
|
84
|
+
const out = new PineMap();
|
|
85
|
+
out.restoreContents(this.snapshotContents());
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Deep-cloned entries for realtime tick rollback. */
|
|
90
|
+
snapshotContents(): { data: Array<[string, Cell]>; order: string[] } {
|
|
91
|
+
const data: Array<[string, Cell]> = [];
|
|
92
|
+
for (const [k, v] of this.data) data.push([k, cloneValue(v) as Cell]);
|
|
93
|
+
return { data, order: [...this.order] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Replace entries with clones of `snap` — same instance for aliases. */
|
|
97
|
+
restoreContents(snap: { data: Array<[string, Cell]>; order: string[] }): void {
|
|
98
|
+
this.data.clear();
|
|
99
|
+
this.order.length = 0;
|
|
100
|
+
for (const [k, v] of snap.data) this.data.set(k, cloneValue(v) as Cell);
|
|
101
|
+
for (const k of snap.order) this.order.push(k);
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
putAll(other: PineMap): void {
|
|
80
105
|
if (!(other instanceof PineMap)) return;
|
|
81
106
|
const keys = other.keys();
|