@hoox-sh/pynets 0.1.0 → 0.3.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 +59 -0
- package/README.md +20 -9
- package/dist/browser.js +95 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23002 -12427
- package/package.json +1 -1
- package/src/ast/builder.ts +235 -12
- package/src/ast/nodes.ts +114 -4
- package/src/ast/unparse.ts +192 -17
- package/src/cli/rich.ts +4 -3
- package/src/cli.ts +29 -7
- package/src/generated/PinescriptLexer.interp +5 -1
- package/src/generated/PinescriptLexer.tokens +97 -95
- package/src/generated/PinescriptLexer.ts +512 -502
- package/src/generated/PinescriptParser.interp +8 -1
- package/src/generated/PinescriptParser.tokens +97 -95
- package/src/generated/PinescriptParser.ts +1907 -1287
- package/src/generated/PinescriptParserVisitor.ts +36 -1
- package/src/index.ts +22 -0
- package/src/runtime/array.ts +238 -68
- package/src/runtime/compile/collections.ts +492 -0
- package/src/runtime/compile/draw.ts +181 -0
- package/src/runtime/compile/emit.ts +1421 -0
- package/src/runtime/compile/emit_call.ts +1796 -0
- package/src/runtime/compile/emit_udf.ts +326 -0
- package/src/runtime/compile/engine.ts +265 -0
- package/src/runtime/compile/helpers.ts +140 -0
- package/src/runtime/compile/index.ts +33 -0
- package/src/runtime/compile/runtime.ts +357 -0
- package/src/runtime/compile/strategy.ts +250 -0
- package/src/runtime/compile/types.ts +127 -0
- package/src/runtime/compile/udt_runtime.ts +50 -0
- package/src/runtime/drawings.ts +405 -8
- package/src/runtime/interpret.ts +2387 -281
- package/src/runtime/matrix.ts +371 -42
- package/src/runtime/strategy.ts +695 -33
- package/src/runtime/ta.ts +654 -20
- package/src/runtime/timeframe.ts +199 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* UDF FunctionDef emission for JS compile.
|
|
6
|
+
* Signature: `function name(__bar_idx, __h, ...formals, ...__st_*)`.
|
|
7
|
+
* Series formals are full arrays (no default). Series locals persist in
|
|
8
|
+
* `__st_${safeFunc}_${safeLocal}`. Last Expr (and last Assign/ReAssign)
|
|
9
|
+
* becomes the Pine return value.
|
|
10
|
+
*/
|
|
11
|
+
import type { Assign, Expr, FunctionDef, Param, ReAssign, expr, stmt } from "../../ast/nodes.ts";
|
|
12
|
+
import type { EmitCtx } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
/** JS / emit keywords that cannot be used as identifiers. Pine `from` included. */
|
|
15
|
+
const RESERVED = new Set([
|
|
16
|
+
"arguments",
|
|
17
|
+
"await",
|
|
18
|
+
"break",
|
|
19
|
+
"case",
|
|
20
|
+
"catch",
|
|
21
|
+
"class",
|
|
22
|
+
"const",
|
|
23
|
+
"continue",
|
|
24
|
+
"debugger",
|
|
25
|
+
"default",
|
|
26
|
+
"delete",
|
|
27
|
+
"do",
|
|
28
|
+
"else",
|
|
29
|
+
"enum",
|
|
30
|
+
"eval",
|
|
31
|
+
"export",
|
|
32
|
+
"extends",
|
|
33
|
+
"false",
|
|
34
|
+
"finally",
|
|
35
|
+
"for",
|
|
36
|
+
"from",
|
|
37
|
+
"function",
|
|
38
|
+
"if",
|
|
39
|
+
"implements",
|
|
40
|
+
"import",
|
|
41
|
+
"in",
|
|
42
|
+
"instanceof",
|
|
43
|
+
"interface",
|
|
44
|
+
"let",
|
|
45
|
+
"new",
|
|
46
|
+
"null",
|
|
47
|
+
"of",
|
|
48
|
+
"package",
|
|
49
|
+
"private",
|
|
50
|
+
"protected",
|
|
51
|
+
"public",
|
|
52
|
+
"return",
|
|
53
|
+
"static",
|
|
54
|
+
"super",
|
|
55
|
+
"switch",
|
|
56
|
+
"this",
|
|
57
|
+
"throw",
|
|
58
|
+
"true",
|
|
59
|
+
"try",
|
|
60
|
+
"typeof",
|
|
61
|
+
"var",
|
|
62
|
+
"void",
|
|
63
|
+
"while",
|
|
64
|
+
"with",
|
|
65
|
+
"yield",
|
|
66
|
+
"__bar_idx",
|
|
67
|
+
"__h",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/** Pine UDF / formal → ordered `__st_*` idents (same order as the def tail). */
|
|
71
|
+
const funcStArrays = new WeakMap<EmitCtx, Map<string, string[]>>();
|
|
72
|
+
|
|
73
|
+
function safeIdent(name: string): string {
|
|
74
|
+
if (!name) return "_";
|
|
75
|
+
let s = /^[A-Za-z_$]/.test(name) ? name : `_${name}`;
|
|
76
|
+
s = s.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
77
|
+
return RESERVED.has(s) ? `${s}_` : s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Persistent series-local array: `__st_${safeFunc}_${safeLocal}`. */
|
|
81
|
+
function stIdent(funcSafe: string, localPine: string): string {
|
|
82
|
+
return `__st_${funcSafe}_${safeIdent(localPine)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stripSemi(code: string): string {
|
|
86
|
+
return code.trim().replace(/;+\s*$/, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function indentBody(code: string): string {
|
|
90
|
+
return code
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map((ln) => (ln.length ? ` ${ln}` : ln))
|
|
93
|
+
.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureSemi(code: string): string {
|
|
97
|
+
const t = code.trimEnd();
|
|
98
|
+
if (!t) return t;
|
|
99
|
+
if (/[;{}]$/.test(t)) return t;
|
|
100
|
+
return `${t};`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const CTRL_KINDS = new Set(["If", "ForTo", "ForIn", "While", "Switch"]);
|
|
104
|
+
|
|
105
|
+
function isCtrlExpr(node: stmt): boolean {
|
|
106
|
+
return node.kind === "Expr" && CTRL_KINDS.has(node.value.kind);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isVarMode(mode: Assign["mode"]): boolean {
|
|
110
|
+
if (mode == null) return false;
|
|
111
|
+
const k = typeof mode === "object" && mode != null && "kind" in mode ? mode.kind : String(mode);
|
|
112
|
+
return k === "Var" || k === "VarIp";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function emitDefault(param: Param, visitStmt: (s: stmt) => string): string {
|
|
116
|
+
if (param.default == null) return "null";
|
|
117
|
+
return visitExpr(param.default, visitStmt);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function visitExpr(value: expr, visitStmt: (s: stmt) => string): string {
|
|
121
|
+
const dummy: Expr = { kind: "Expr", value };
|
|
122
|
+
const visited = stripSemi(visitStmt(dummy));
|
|
123
|
+
return visited || "null";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function lastAssignReturn(node: Assign | ReAssign, localSt: Map<string, string>): string {
|
|
127
|
+
const target = node.target;
|
|
128
|
+
if (target.kind === "Name") {
|
|
129
|
+
const st = localSt.get(target.id);
|
|
130
|
+
if (st != null) return ` return ${st}[__bar_idx];`;
|
|
131
|
+
return ` return ${safeIdent(target.id)};`;
|
|
132
|
+
}
|
|
133
|
+
return " return null;";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function asReturn(code: string): string {
|
|
137
|
+
const t = stripSemi(code);
|
|
138
|
+
if (!t) return " return null;";
|
|
139
|
+
if (/^return\b/.test(t)) return indentBody(ensureSemi(t));
|
|
140
|
+
return ` return ${t};`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function walkFnBody(node: unknown, visit: (n: { kind?: string; [k: string]: unknown }) => void): void {
|
|
144
|
+
if (node == null || typeof node !== "object") return;
|
|
145
|
+
if (Array.isArray(node)) {
|
|
146
|
+
for (const item of node) walkFnBody(item, visit);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const n = node as { kind?: string; [k: string]: unknown };
|
|
150
|
+
if (n.kind === "FunctionDef" || n.kind === "TypeDef" || n.kind === "EnumDef") {
|
|
151
|
+
visit(n);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (typeof n.kind === "string") visit(n);
|
|
155
|
+
for (const v of Object.values(n)) walkFnBody(v, visit);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function addTargetNames(target: unknown, into: Set<string>): void {
|
|
159
|
+
if (target == null || typeof target !== "object") return;
|
|
160
|
+
const t = target as { kind?: string; id?: string; elts?: unknown[] };
|
|
161
|
+
if (t.kind === "Name" && typeof t.id === "string") {
|
|
162
|
+
into.add(t.id);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (t.kind === "Tuple") {
|
|
166
|
+
for (const el of t.elts ?? []) addTargetNames(el, into);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rememberSt(ctx: EmitCtx, pine: string, safe: string, st: string[]): void {
|
|
171
|
+
let m = funcStArrays.get(ctx);
|
|
172
|
+
if (m == null) {
|
|
173
|
+
m = new Map();
|
|
174
|
+
funcStArrays.set(ctx, m);
|
|
175
|
+
}
|
|
176
|
+
m.set(pine, st);
|
|
177
|
+
if (safe !== pine) m.set(safe, st);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function stForFunc(ctx: EmitCtx, pine: string, safe: string): string[] {
|
|
181
|
+
const mapped = funcStArrays.get(ctx)?.get(pine) ?? funcStArrays.get(ctx)?.get(safe);
|
|
182
|
+
if (mapped != null) return mapped;
|
|
183
|
+
const prefix = `__st_${safe}_`;
|
|
184
|
+
return [...ctx.stArrays].filter((a) => a.startsWith(prefix)).sort();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formalPineNames(node: FunctionDef): string[] {
|
|
188
|
+
const params = Array.isArray(node.args) ? node.args : [];
|
|
189
|
+
const names: string[] = [];
|
|
190
|
+
for (let i = 0; i < params.length; i++) {
|
|
191
|
+
const p = params[i]!;
|
|
192
|
+
names.push(node.method && i === 0 && !p.name ? "this" : p.name);
|
|
193
|
+
}
|
|
194
|
+
return names;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function emitFunctionDef(ctx: EmitCtx, node: FunctionDef, visitStmt: (s: stmt) => string): void {
|
|
198
|
+
const pineName = node.name;
|
|
199
|
+
ctx.userFuncs.add(pineName);
|
|
200
|
+
const safe = safeIdent(pineName);
|
|
201
|
+
if (safe !== pineName) ctx.userFuncs.add(safe);
|
|
202
|
+
|
|
203
|
+
const params = Array.isArray(node.args) ? node.args : [];
|
|
204
|
+
const pineParams = formalPineNames(node);
|
|
205
|
+
ctx.funcParamNames.set(pineName, pineParams);
|
|
206
|
+
if (safe !== pineName) ctx.funcParamNames.set(safe, pineParams);
|
|
207
|
+
|
|
208
|
+
const paramSet = new Set(pineParams);
|
|
209
|
+
const body = Array.isArray(node.body) ? node.body : [];
|
|
210
|
+
|
|
211
|
+
const seriesSet = new Set<string>();
|
|
212
|
+
const assigned = new Set<string>();
|
|
213
|
+
const varLocals = new Set<string>();
|
|
214
|
+
const historyNames = new Set<string>();
|
|
215
|
+
const varDefaults = new Map<string, expr | null>();
|
|
216
|
+
|
|
217
|
+
walkFnBody(body, (n) => {
|
|
218
|
+
if (n.kind === "Subscript") {
|
|
219
|
+
const value = n.value as { kind?: string; id?: string } | undefined;
|
|
220
|
+
if (value?.kind === "Name" && typeof value.id === "string") {
|
|
221
|
+
historyNames.add(value.id);
|
|
222
|
+
if (paramSet.has(value.id)) seriesSet.add(value.id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (n.kind === "Assign" || n.kind === "ReAssign") {
|
|
226
|
+
addTargetNames(n.target, assigned);
|
|
227
|
+
if (n.kind === "Assign" && isVarMode(n.mode as Assign["mode"])) {
|
|
228
|
+
const names = new Set<string>();
|
|
229
|
+
addTargetNames(n.target, names);
|
|
230
|
+
for (const id of names) {
|
|
231
|
+
if (paramSet.has(id)) continue;
|
|
232
|
+
varLocals.add(id);
|
|
233
|
+
if (!varDefaults.has(id)) varDefaults.set(id, (n.value as expr | null) ?? null);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
ctx.funcSeriesParams.set(pineName, seriesSet);
|
|
240
|
+
if (safe !== pineName) ctx.funcSeriesParams.set(safe, seriesSet);
|
|
241
|
+
|
|
242
|
+
const seriesLocals = [...new Set([...varLocals, ...historyNames])]
|
|
243
|
+
.filter((id) => assigned.has(id) && !paramSet.has(id))
|
|
244
|
+
.sort();
|
|
245
|
+
|
|
246
|
+
const localSt = new Map<string, string>();
|
|
247
|
+
const stNames: string[] = [];
|
|
248
|
+
for (const s of seriesLocals) {
|
|
249
|
+
const st = stIdent(safe, s);
|
|
250
|
+
ctx.stArrays.add(st);
|
|
251
|
+
localSt.set(s, st);
|
|
252
|
+
stNames.push(st);
|
|
253
|
+
}
|
|
254
|
+
rememberSt(ctx, pineName, safe, stNames);
|
|
255
|
+
|
|
256
|
+
const prevFunc = ctx.currentFunc;
|
|
257
|
+
const prevParams = ctx.currentParamNames;
|
|
258
|
+
const prevSeries = ctx.currentSeriesParams;
|
|
259
|
+
ctx.currentFunc = pineName;
|
|
260
|
+
ctx.currentParamNames = new Set(pineParams);
|
|
261
|
+
ctx.currentSeriesParams = seriesSet;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const formals: string[] = [];
|
|
265
|
+
for (let i = 0; i < params.length; i++) {
|
|
266
|
+
const pine = pineParams[i]!;
|
|
267
|
+
const ident = safeIdent(pine);
|
|
268
|
+
if (seriesSet.has(pine)) formals.push(ident);
|
|
269
|
+
else formals.push(`${ident} = ${emitDefault(params[i]!, visitStmt)}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lines: string[] = [];
|
|
273
|
+
for (const s of seriesLocals) {
|
|
274
|
+
const st = localSt.get(s)!;
|
|
275
|
+
const init = varDefaults.get(s);
|
|
276
|
+
const def = init != null ? visitExpr(init, visitStmt) : "null";
|
|
277
|
+
lines.push(` if (__bar_idx === 0) ${st}[0] = ${def};`);
|
|
278
|
+
lines.push(` else ${st}[__bar_idx] = ${st}[__bar_idx - 1];`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const visitBody = (s: stmt): string => {
|
|
282
|
+
if (s.kind !== "Assign" && s.kind !== "ReAssign") return visitStmt(s);
|
|
283
|
+
if (s.target.kind !== "Name" || !localSt.has(s.target.id)) return visitStmt(s);
|
|
284
|
+
const st = localSt.get(s.target.id)!;
|
|
285
|
+
if (s.kind === "Assign" && isVarMode(s.mode)) return "";
|
|
286
|
+
const rhs = s.value != null ? visitExpr(s.value, visitStmt) : "null";
|
|
287
|
+
return `${st}[__bar_idx] = __h.naNum(${rhs});`;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
let emittedReturn = false;
|
|
291
|
+
for (let i = 0; i < body.length; i++) {
|
|
292
|
+
const s = body[i]!;
|
|
293
|
+
const raw = visitBody(s);
|
|
294
|
+
if (!raw || !raw.trim()) continue;
|
|
295
|
+
const isLast = i === body.length - 1;
|
|
296
|
+
if (isLast && s.kind === "Expr" && !isCtrlExpr(s)) {
|
|
297
|
+
lines.push(asReturn(raw));
|
|
298
|
+
emittedReturn = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
lines.push(indentBody(ensureSemi(raw)));
|
|
302
|
+
if (isLast && (s.kind === "Assign" || s.kind === "ReAssign")) {
|
|
303
|
+
lines.push(lastAssignReturn(s, localSt));
|
|
304
|
+
emittedReturn = true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!emittedReturn) lines.push(" return null;");
|
|
308
|
+
|
|
309
|
+
const argList = ["__bar_idx", "__h", ...formals, ...stNames].join(", ");
|
|
310
|
+
ctx.functions.push(`function ${safe}(${argList}) {\n${lines.join("\n")}\n}`);
|
|
311
|
+
} finally {
|
|
312
|
+
ctx.currentFunc = prevFunc;
|
|
313
|
+
ctx.currentParamNames = prevParams;
|
|
314
|
+
ctx.currentSeriesParams = prevSeries;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function emitUserFuncCall(ctx: EmitCtx, name: string, args: string[]): string {
|
|
319
|
+
const pine = ctx.userFuncs.has(name)
|
|
320
|
+
? name
|
|
321
|
+
: ([...ctx.userFuncs].find((n) => safeIdent(n) === name) ?? name);
|
|
322
|
+
const safe = safeIdent(pine);
|
|
323
|
+
const st = stForFunc(ctx, pine, safe);
|
|
324
|
+
const parts = ["__bar_idx", "__h", ...args, ...st];
|
|
325
|
+
return `${safe}(${parts.join(", ")})`;
|
|
326
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Compile engine: eligibility, transpile, sha256 LRU (max 128), load, run.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import type {
|
|
9
|
+
CompileCacheStats,
|
|
10
|
+
CompileEligibility,
|
|
11
|
+
CompiledScript,
|
|
12
|
+
CompilePlotMeta,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
import { CompileEmitError, CompileIneligibleError, CompileLoadError } from "./types.ts";
|
|
15
|
+
import { transpileSource } from "./emit.ts";
|
|
16
|
+
import { defaultTime, defaultVolume, naNum, toFloatArr } from "./helpers.ts";
|
|
17
|
+
import { createCompileHelpers } from "./runtime.ts";
|
|
18
|
+
import type { CompileHelperInputs } from "./runtime.ts";
|
|
19
|
+
import type { DrawingEvent } from "../drawings.ts";
|
|
20
|
+
import type { LogRecord } from "../log.ts";
|
|
21
|
+
import type { Fill, StrategyEvent, StrategySummary } from "../strategy.ts";
|
|
22
|
+
|
|
23
|
+
export type CompileRunExtras = {
|
|
24
|
+
inputs?: CompileHelperInputs;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CACHE_MAX = 128;
|
|
28
|
+
const cache = new Map<string, CompiledScript>();
|
|
29
|
+
|
|
30
|
+
export type OhlcvBar = {
|
|
31
|
+
open?: number | null;
|
|
32
|
+
high?: number | null;
|
|
33
|
+
low?: number | null;
|
|
34
|
+
close?: number | null;
|
|
35
|
+
volume?: number | null;
|
|
36
|
+
time?: number | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type CompileHostResult = {
|
|
40
|
+
series: Record<string, Array<number | null>>;
|
|
41
|
+
plots: Array<number | null>;
|
|
42
|
+
plot_meta: Array<{ title: string }>;
|
|
43
|
+
count: number;
|
|
44
|
+
script_name: null;
|
|
45
|
+
script_type: "indicator" | "strategy";
|
|
46
|
+
mode: "compile";
|
|
47
|
+
events?: StrategyEvent[];
|
|
48
|
+
fills?: Fill[];
|
|
49
|
+
strategy?: StrategySummary;
|
|
50
|
+
drawings?: DrawingEvent[];
|
|
51
|
+
logs?: LogRecord[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Generated `execute_script_compiled` — helpers are injected per `run()`, not cached. */
|
|
55
|
+
type CompiledExecuteFn = (
|
|
56
|
+
o: Array<number | null>,
|
|
57
|
+
h: Array<number | null>,
|
|
58
|
+
l: Array<number | null>,
|
|
59
|
+
c: Array<number | null>,
|
|
60
|
+
v: Array<number | null>,
|
|
61
|
+
t: Array<number | null>,
|
|
62
|
+
helpers: unknown,
|
|
63
|
+
) => Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
export function compileEligible(source: string): CompileEligibility {
|
|
66
|
+
// Python compile_script accepts `import` (library stubs in object mode).
|
|
67
|
+
// Only the auto-mode host prefilter rejects import — not this gate.
|
|
68
|
+
// request.security is also eligible (same-symbol passthrough / else na).
|
|
69
|
+
// Empty source is still ok; emit/load report later failures.
|
|
70
|
+
void source;
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CompileScriptExtras = {
|
|
75
|
+
getLibrary?: (namespace: string, name: string, version: number) => string | null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function transpile(source: string, extras?: CompileScriptExtras): string {
|
|
79
|
+
return transpileSource(source, extras).code;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function clearCompileCache(): void {
|
|
83
|
+
cache.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function compileCacheStats(): CompileCacheStats {
|
|
87
|
+
return { source_entries: cache.size, source_max: CACHE_MAX };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sha256(s: string): string {
|
|
91
|
+
return createHash("sha256").update(s).digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function loadExecute(code: string): CompiledExecuteFn {
|
|
95
|
+
let factory: () => CompiledExecuteFn;
|
|
96
|
+
try {
|
|
97
|
+
factory = new Function(`${code}\nreturn execute_script_compiled;`) as typeof factory;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
throw new CompileLoadError(msg);
|
|
101
|
+
}
|
|
102
|
+
const fn = factory();
|
|
103
|
+
if (typeof fn !== "function") throw new CompileLoadError("execute_script_compiled missing");
|
|
104
|
+
return fn;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assertSameLength(n: number, ...lens: number[]): void {
|
|
108
|
+
for (const len of lens) {
|
|
109
|
+
if (len !== n) throw new Error("OHLCV arrays must have the same length");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractOhlcv(bars: Array<OhlcvBar>): {
|
|
114
|
+
open: Array<number | null>;
|
|
115
|
+
high: Array<number | null>;
|
|
116
|
+
low: Array<number | null>;
|
|
117
|
+
close: Array<number | null>;
|
|
118
|
+
volume: Array<number | null>;
|
|
119
|
+
time: Array<number | null>;
|
|
120
|
+
} {
|
|
121
|
+
const n = bars.length;
|
|
122
|
+
const open: Array<number | null> = new Array(n);
|
|
123
|
+
const high: Array<number | null> = new Array(n);
|
|
124
|
+
const low: Array<number | null> = new Array(n);
|
|
125
|
+
const close: Array<number | null> = new Array(n);
|
|
126
|
+
const volume: Array<number | null> = new Array(n);
|
|
127
|
+
const time: Array<number | null> = new Array(n);
|
|
128
|
+
for (let i = 0; i < n; i++) {
|
|
129
|
+
const b = bars[i] ?? {};
|
|
130
|
+
open[i] = b.open ?? null;
|
|
131
|
+
high[i] = b.high ?? null;
|
|
132
|
+
low[i] = b.low ?? null;
|
|
133
|
+
close[i] = b.close ?? null;
|
|
134
|
+
volume[i] = b.volume ?? 1;
|
|
135
|
+
time[i] = b.time ?? i * 60_000;
|
|
136
|
+
}
|
|
137
|
+
assertSameLength(n, open.length, high.length, low.length, close.length, volume.length, time.length);
|
|
138
|
+
return { open, high, low, close, volume, time };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class JsCompiledScript implements CompiledScript {
|
|
142
|
+
readonly backend = "js" as const;
|
|
143
|
+
|
|
144
|
+
constructor(
|
|
145
|
+
readonly source: string,
|
|
146
|
+
readonly plots: CompilePlotMeta[],
|
|
147
|
+
private readonly fn: CompiledExecuteFn,
|
|
148
|
+
) {}
|
|
149
|
+
|
|
150
|
+
run(
|
|
151
|
+
open: ArrayLike<number | null | undefined>,
|
|
152
|
+
high: ArrayLike<number | null | undefined>,
|
|
153
|
+
low: ArrayLike<number | null | undefined>,
|
|
154
|
+
close: ArrayLike<number | null | undefined>,
|
|
155
|
+
volume?: ArrayLike<number | null | undefined> | null,
|
|
156
|
+
time?: ArrayLike<number | null | undefined> | null,
|
|
157
|
+
extras?: CompileRunExtras,
|
|
158
|
+
): Record<string, Array<number | null>> {
|
|
159
|
+
const n = close.length;
|
|
160
|
+
assertSameLength(n, open.length, high.length, low.length);
|
|
161
|
+
if (volume != null) assertSameLength(n, volume.length);
|
|
162
|
+
if (time != null) assertSameLength(n, time.length);
|
|
163
|
+
const o = toFloatArr(open, n, null);
|
|
164
|
+
const h = toFloatArr(high, n, null);
|
|
165
|
+
const l = toFloatArr(low, n, null);
|
|
166
|
+
const c = toFloatArr(close, n, null);
|
|
167
|
+
const v = volume != null ? toFloatArr(volume, n, 1) : defaultVolume(n);
|
|
168
|
+
const t = time != null ? toFloatArr(time, n, null) : defaultTime(n);
|
|
169
|
+
for (let i = 0; i < n; i++) {
|
|
170
|
+
if (v[i] == null) v[i] = 1;
|
|
171
|
+
if (t[i] == null) t[i] = i * 60_000;
|
|
172
|
+
}
|
|
173
|
+
return this.fn(o, h, l, c, v, t, createCompileHelpers({ inputs: extras?.inputs })) as Record<
|
|
174
|
+
string,
|
|
175
|
+
Array<number | null>
|
|
176
|
+
>;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function libraryCacheTail(
|
|
181
|
+
source: string,
|
|
182
|
+
getLibrary?: CompileScriptExtras["getLibrary"],
|
|
183
|
+
): string {
|
|
184
|
+
if (getLibrary == null) return "";
|
|
185
|
+
const parts: string[] = [];
|
|
186
|
+
for (const m of source.matchAll(/import\s+(\S+)\/(\S+)\/(\d+)/g)) {
|
|
187
|
+
parts.push(getLibrary(m[1]!, m[2]!, Number(m[3])) ?? "");
|
|
188
|
+
}
|
|
189
|
+
return `\0${parts.join("\0")}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function compileScript(source: string, extras?: CompileScriptExtras): CompiledScript {
|
|
193
|
+
const elig = compileEligible(source);
|
|
194
|
+
if (!elig.ok) throw new CompileIneligibleError(elig.reason ?? "ineligible");
|
|
195
|
+
const key = sha256(source + libraryCacheTail(source, extras?.getLibrary));
|
|
196
|
+
const hit = cache.get(key);
|
|
197
|
+
if (hit) {
|
|
198
|
+
cache.delete(key);
|
|
199
|
+
cache.set(key, hit);
|
|
200
|
+
return hit;
|
|
201
|
+
}
|
|
202
|
+
let code: string;
|
|
203
|
+
let ctx: { errors: string[]; plots: CompilePlotMeta[] };
|
|
204
|
+
try {
|
|
205
|
+
({ code, ctx } = transpileSource(source, extras));
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
throw new CompileEmitError(msg);
|
|
209
|
+
}
|
|
210
|
+
if (ctx.errors.length) throw new CompileEmitError(ctx.errors.join("; "));
|
|
211
|
+
if (!code.includes("execute_script_compiled")) {
|
|
212
|
+
throw new CompileEmitError("empty emit");
|
|
213
|
+
}
|
|
214
|
+
const exec = loadExecute(code);
|
|
215
|
+
const compiled = new JsCompiledScript(code, ctx.plots, exec);
|
|
216
|
+
cache.set(key, compiled);
|
|
217
|
+
if (cache.size > CACHE_MAX) {
|
|
218
|
+
const first = cache.keys().next().value;
|
|
219
|
+
if (first != null) cache.delete(first);
|
|
220
|
+
}
|
|
221
|
+
return compiled;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function runScript(
|
|
225
|
+
source: string,
|
|
226
|
+
ohlcvBars: Array<OhlcvBar>,
|
|
227
|
+
): Record<string, Array<number | null>> {
|
|
228
|
+
const cols = extractOhlcv(ohlcvBars);
|
|
229
|
+
return compileScript(source).run(cols.open, cols.high, cols.low, cols.close, cols.volume, cols.time);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function compileToResult(
|
|
233
|
+
source: string,
|
|
234
|
+
ohlcvBars: Array<OhlcvBar>,
|
|
235
|
+
extras?: CompileRunExtras,
|
|
236
|
+
): CompileHostResult {
|
|
237
|
+
const compiled = compileScript(source);
|
|
238
|
+
const cols = extractOhlcv(ohlcvBars);
|
|
239
|
+
const raw = compiled.run(cols.open, cols.high, cols.low, cols.close, cols.volume, cols.time, extras);
|
|
240
|
+
const series: Record<string, Array<number | null>> = {};
|
|
241
|
+
const blob: Record<string, unknown> = {};
|
|
242
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
243
|
+
if (key.startsWith("__")) blob[key] = value;
|
|
244
|
+
else if (Array.isArray(value)) series[key] = value;
|
|
245
|
+
}
|
|
246
|
+
const plot_meta = compiled.plots;
|
|
247
|
+
const firstTitle = plot_meta.find((p) => !p.title.startsWith("__"))?.title;
|
|
248
|
+
const result: CompileHostResult = {
|
|
249
|
+
series,
|
|
250
|
+
plots: firstTitle != null ? (series[firstTitle] ?? []) : [],
|
|
251
|
+
plot_meta,
|
|
252
|
+
count: ohlcvBars.length,
|
|
253
|
+
script_name: null,
|
|
254
|
+
script_type: "__events" in blob ? "strategy" : "indicator",
|
|
255
|
+
mode: "compile",
|
|
256
|
+
};
|
|
257
|
+
if ("__events" in blob) result.events = blob.__events as StrategyEvent[];
|
|
258
|
+
if ("__fills" in blob) result.fills = blob.__fills as Fill[];
|
|
259
|
+
if ("__strategy" in blob) result.strategy = blob.__strategy as StrategySummary;
|
|
260
|
+
if ("__drawings" in blob) result.drawings = blob.__drawings as DrawingEvent[];
|
|
261
|
+
if ("__logs" in blob) result.logs = blob.__logs as LogRecord[];
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { naNum };
|