@hoox-sh/pynets 0.9.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 +15 -0
- package/README.md +9 -1
- package/dist/browser.js +63 -63
- package/dist/index.js +382 -37
- 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 +259 -40
- 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
|
@@ -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
|
@@ -220,6 +220,12 @@ export interface RuntimeOptions {
|
|
|
220
220
|
timeout_seconds?: number | null;
|
|
221
221
|
/** Visit the last bar as a forming realtime bar (`barstate.isrealtime`). */
|
|
222
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;
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
export interface RuntimeFill {
|
|
@@ -364,8 +370,18 @@ interface Env {
|
|
|
364
370
|
maxBarsBackDecls: Array<{ var: Value; num: Value }>;
|
|
365
371
|
/** Python `_alert_fire_bars` keys for `alert` / `alertcondition` freq gating. */
|
|
366
372
|
alertFireBars: Map<string, number>;
|
|
367
|
-
/**
|
|
368
|
-
|
|
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>;
|
|
369
385
|
}
|
|
370
386
|
|
|
371
387
|
export class Runtime {
|
|
@@ -417,6 +433,12 @@ export class Runtime {
|
|
|
417
433
|
const extraOpts = extra && !isPlainInputs(extra) ? extra : undefined;
|
|
418
434
|
const mode = extraOpts?.mode ?? this.mode;
|
|
419
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
|
+
});
|
|
420
442
|
const host = {
|
|
421
443
|
symbol: this.symbol,
|
|
422
444
|
inputs,
|
|
@@ -424,7 +446,8 @@ export class Runtime {
|
|
|
424
446
|
timeframe: extraOpts?.timeframe ?? this.timeframe,
|
|
425
447
|
libraries: extraOpts?.libraries != null ? resolveLibraries(extraOpts.libraries) : this.libraries,
|
|
426
448
|
timeout_seconds: extraOpts?.timeout_seconds ?? this.timeout_seconds,
|
|
427
|
-
|
|
449
|
+
realtime_first: rt.first,
|
|
450
|
+
realtime_ticks: rt.ticks,
|
|
428
451
|
};
|
|
429
452
|
if (mode === "compile") {
|
|
430
453
|
return runCompiled(source, ohlcv, host);
|
|
@@ -549,6 +572,126 @@ export function interpret(source: string, ohlcv: OHLCVBar[]): { plots: Array<num
|
|
|
549
572
|
return { plots: out.plots };
|
|
550
573
|
}
|
|
551
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
|
+
|
|
552
695
|
export function interpretTree(
|
|
553
696
|
tree: AST,
|
|
554
697
|
ohlcv: OHLCVBar[],
|
|
@@ -559,7 +702,8 @@ export function interpretTree(
|
|
|
559
702
|
timeframe?: string | null;
|
|
560
703
|
libraries?: LibraryRegistry;
|
|
561
704
|
timeout_seconds?: number | null;
|
|
562
|
-
|
|
705
|
+
realtime_first?: number | null;
|
|
706
|
+
realtime_ticks?: number;
|
|
563
707
|
},
|
|
564
708
|
): RuntimeResult {
|
|
565
709
|
const scriptNode = tree.kind === "Script" ? (tree as Script) : null;
|
|
@@ -598,7 +742,12 @@ export function interpretTree(
|
|
|
598
742
|
onceFired: new WeakSet(),
|
|
599
743
|
maxBarsBackDecls: [],
|
|
600
744
|
alertFireBars: new Map(),
|
|
601
|
-
|
|
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(),
|
|
602
751
|
};
|
|
603
752
|
|
|
604
753
|
const n = ohlcv.length;
|
|
@@ -608,8 +757,9 @@ export function interpretTree(
|
|
|
608
757
|
const timeoutSec = host?.timeout_seconds;
|
|
609
758
|
const deadline =
|
|
610
759
|
timeoutSec != null && Number.isFinite(timeoutSec) ? performance.now() + timeoutSec * 1000 : null;
|
|
760
|
+
let aborted = false;
|
|
611
761
|
try {
|
|
612
|
-
for (let i = 0; i < n; i++) {
|
|
762
|
+
for (let i = 0; i < n && !aborted; i++) {
|
|
613
763
|
if (deadline != null && (i & 31) === 0 && performance.now() > deadline) {
|
|
614
764
|
timedOut = true;
|
|
615
765
|
barsDone = i;
|
|
@@ -629,31 +779,44 @@ export function interpretTree(
|
|
|
629
779
|
num(env.ctx.close) ?? 0,
|
|
630
780
|
);
|
|
631
781
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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;
|
|
645
807
|
}
|
|
646
|
-
|
|
808
|
+
execStmt(stmt, env);
|
|
647
809
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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;
|
|
652
818
|
}
|
|
653
|
-
|
|
654
|
-
if (err === LOOP_BREAK || err === LOOP_CONTINUE) continue;
|
|
655
|
-
runError = { error: formatRunError(err), error_kind: "runtime" };
|
|
656
|
-
break;
|
|
819
|
+
if (snap) restoreTickState(env, snap);
|
|
657
820
|
}
|
|
658
821
|
}
|
|
659
822
|
} catch (err) {
|
|
@@ -931,7 +1094,8 @@ function uniquifyPlotKey(env: Env, node: object, title: string): string {
|
|
|
931
1094
|
}
|
|
932
1095
|
|
|
933
1096
|
function isVarMode(stmt: Assign): boolean {
|
|
934
|
-
|
|
1097
|
+
// `varip` matches `var` on historical bars: init on first execution, persist.
|
|
1098
|
+
return stmt.mode?.kind === "Var" || stmt.mode?.kind === "VarIp";
|
|
935
1099
|
}
|
|
936
1100
|
|
|
937
1101
|
function isTuple(v: Value): v is TupleVal {
|
|
@@ -1055,6 +1219,16 @@ function evalAssign(stmt: Assign, env: Env): Value {
|
|
|
1055
1219
|
}
|
|
1056
1220
|
if (stmt.target.kind !== "Name") return NA;
|
|
1057
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
|
+
}
|
|
1058
1232
|
// var: init on first execution (bar_index==0, or first time the name is missing).
|
|
1059
1233
|
if (isVarMode(stmt) && (inited.has(id) || (env.barIndex !== 0 && ctxGet(env, id) !== undefined))) {
|
|
1060
1234
|
inited.add(id);
|
|
@@ -1309,6 +1483,14 @@ function evalExpr(node: expr | undefined, env: Env): Value {
|
|
|
1309
1483
|
if (node.value.kind === "Name") {
|
|
1310
1484
|
const s = seriesGet(env, node.value.id);
|
|
1311
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
|
+
}
|
|
1312
1494
|
}
|
|
1313
1495
|
return NA;
|
|
1314
1496
|
}
|
|
@@ -1687,14 +1869,14 @@ function evalAttribute(node: Attribute, env: Env): Value {
|
|
|
1687
1869
|
}
|
|
1688
1870
|
if (node.value.id === "barstate") {
|
|
1689
1871
|
const last = env.barIndex === env.barCount - 1;
|
|
1690
|
-
const
|
|
1872
|
+
const rt = env.barRealtime;
|
|
1691
1873
|
if (node.attr === "isfirst") return env.barIndex === 0 ? 1 : 0;
|
|
1692
1874
|
if (node.attr === "islast") return last ? 1 : 0;
|
|
1693
|
-
if (node.attr === "ishistory") return
|
|
1694
|
-
if (node.attr === "isconfirmed") return 1;
|
|
1695
|
-
if (node.attr === "isnew") return 1;
|
|
1696
|
-
if (node.attr === "isrealtime") return
|
|
1697
|
-
if (node.attr === "islastconfirmedhistory") return
|
|
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;
|
|
1698
1880
|
}
|
|
1699
1881
|
if (node.value.id === "session") {
|
|
1700
1882
|
if (node.attr === "regular") return "regular";
|
|
@@ -2499,6 +2681,39 @@ function formatRunError(err: unknown): string {
|
|
|
2499
2681
|
return "runtime error";
|
|
2500
2682
|
}
|
|
2501
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
|
+
|
|
2502
2717
|
function isPlainInputs(extra: RuntimeOptions | InputOverrides): extra is InputOverrides {
|
|
2503
2718
|
return (
|
|
2504
2719
|
!("inputs" in extra) &&
|
|
@@ -2507,7 +2722,10 @@ function isPlainInputs(extra: RuntimeOptions | InputOverrides): extra is InputOv
|
|
|
2507
2722
|
!("libraries" in extra) &&
|
|
2508
2723
|
!("mode" in extra) &&
|
|
2509
2724
|
!("timeout_seconds" in extra) &&
|
|
2510
|
-
!("realtimeLastBar" in extra)
|
|
2725
|
+
!("realtimeLastBar" in extra) &&
|
|
2726
|
+
!("realtimeTicks" in extra) &&
|
|
2727
|
+
!("realtimeBars" in extra) &&
|
|
2728
|
+
!("realtimeFromBar" in extra)
|
|
2511
2729
|
);
|
|
2512
2730
|
}
|
|
2513
2731
|
|
|
@@ -2660,7 +2878,8 @@ function runAuto(
|
|
|
2660
2878
|
timeframe?: string | null;
|
|
2661
2879
|
libraries?: LibraryRegistry;
|
|
2662
2880
|
timeout_seconds?: number | null;
|
|
2663
|
-
|
|
2881
|
+
realtime_first?: number | null;
|
|
2882
|
+
realtime_ticks?: number;
|
|
2664
2883
|
},
|
|
2665
2884
|
): RuntimeResult {
|
|
2666
2885
|
const elig = compileEligible(source);
|
|
@@ -2738,9 +2957,9 @@ function normalizeAlertFreq(freq: unknown): string {
|
|
|
2738
2957
|
return ALERT_FREQ_ALIASES[s] ?? (s ? s : FREQ_ONCE_PER_BAR);
|
|
2739
2958
|
}
|
|
2740
2959
|
|
|
2741
|
-
/** Python `_bar_is_confirmed`:
|
|
2742
|
-
function barIsConfirmed(
|
|
2743
|
-
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;
|
|
2744
2963
|
}
|
|
2745
2964
|
|
|
2746
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();
|
package/src/runtime/matrix.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
|
export type Cell = number | null;
|
|
13
14
|
|
|
@@ -564,6 +565,30 @@ export class PineMatrix {
|
|
|
564
565
|
}
|
|
565
566
|
}
|
|
566
567
|
|
|
568
|
+
/** Deep clone (realtime tick snapshots). */
|
|
569
|
+
clone(): PineMatrix {
|
|
570
|
+
const out = new PineMatrix(this.nRows, this.nCols);
|
|
571
|
+
out.restoreContents(this.snapshotContents());
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Deep-cloned grid + dims for realtime tick rollback. */
|
|
576
|
+
snapshotContents(): { cells: unknown[][]; nRows: number; nCols: number } {
|
|
577
|
+
return {
|
|
578
|
+
cells: this.cells.map((row) => row.map(cloneValue)),
|
|
579
|
+
nRows: this.nRows,
|
|
580
|
+
nCols: this.nCols,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Replace grid + dims with clones of `snap` — same instance for aliases. */
|
|
585
|
+
restoreContents(snap: { cells: unknown[][]; nRows: number; nCols: number }): void {
|
|
586
|
+
this.cells.length = 0;
|
|
587
|
+
for (const row of snap.cells) this.cells.push(row.map(cloneValue));
|
|
588
|
+
this.nRows = snap.nRows;
|
|
589
|
+
this.nCols = snap.nCols;
|
|
590
|
+
}
|
|
591
|
+
|
|
567
592
|
transpose(): PineMatrix {
|
|
568
593
|
const t = new PineMatrix(this.nCols, this.nRows);
|
|
569
594
|
for (let i = 0; i < this.nRows; i++) {
|
package/src/runtime/series.ts
CHANGED
|
@@ -21,6 +21,12 @@ export class PineSeries {
|
|
|
21
21
|
else this.data[this.data.length - 1] = value;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Drop cells beyond `len` (realtime tick rollback). No-op when shorter. */
|
|
25
|
+
truncate(len: number): void {
|
|
26
|
+
if (len < 0) return;
|
|
27
|
+
if (this.data.length > len) this.data.length = len;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
get current(): Cell {
|
|
25
31
|
return this.data.length === 0 ? NA : this.data[this.data.length - 1]!;
|
|
26
32
|
}
|
package/src/runtime/strategy.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* fillEntry from flat / reverse leftover opens a trade; flatten moves it to closed.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { cloneValue } from "./clone.ts";
|
|
15
|
+
|
|
14
16
|
export type StrategyDirection = "long" | "short";
|
|
15
17
|
/** `strategy(..., avg_price_model=)` — stock FIFO reweight / sticky futures AEP. */
|
|
16
18
|
export type AvgPriceModel = "stock" | "futures" | "inverse";
|
|
@@ -441,6 +443,36 @@ export class StrategyBook {
|
|
|
441
443
|
if (settings) this.applyDeclSettings(settings);
|
|
442
444
|
}
|
|
443
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Snapshot all broker state (Python `strategy_state.snapshot()` analog)
|
|
448
|
+
* for realtime tick rollback: events, position, fills, pending orders,
|
|
449
|
+
* trades, and every counter.
|
|
450
|
+
*/
|
|
451
|
+
snapshot(): Record<string, unknown> {
|
|
452
|
+
const out: Record<string, unknown> = {};
|
|
453
|
+
for (const [key, value] of Object.entries(this as unknown as Record<string, unknown>)) {
|
|
454
|
+
if (typeof value === "function") continue;
|
|
455
|
+
out[key] = cloneValue(value);
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Restore from `snapshot()` — arrays refill in place so aliases keep identity. */
|
|
461
|
+
restore(snap: Record<string, unknown>): void {
|
|
462
|
+
const self = this as unknown as Record<string, unknown>;
|
|
463
|
+
for (const [key, value] of Object.entries(snap)) {
|
|
464
|
+
const live = self[key];
|
|
465
|
+
if (Array.isArray(live)) {
|
|
466
|
+
live.length = 0;
|
|
467
|
+
for (const item of value as unknown[]) live.push(cloneValue(item));
|
|
468
|
+
} else if (live !== null && typeof live === "object" && value !== null && typeof value === "object") {
|
|
469
|
+
Object.assign(live, value);
|
|
470
|
+
} else {
|
|
471
|
+
self[key] = cloneValue(value);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
444
476
|
/** Margin locked in open trades: sum(|entry * size|) / leverage (Python `capital_held`). */
|
|
445
477
|
capitalHeld(): number {
|
|
446
478
|
let notional = 0;
|