@hoox-sh/pynets 0.1.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +46 -0
  4. package/README.md +327 -0
  5. package/dist/browser.js +44 -0
  6. package/dist/index.d.ts +348 -0
  7. package/dist/index.js +41741 -0
  8. package/package.json +77 -0
  9. package/src/ast/builder.ts +1002 -0
  10. package/src/ast/dump.ts +43 -0
  11. package/src/ast/helper.ts +77 -0
  12. package/src/ast/nodes.ts +570 -0
  13. package/src/ast/unparse.ts +360 -0
  14. package/src/cli/rich.ts +378 -0
  15. package/src/cli.ts +627 -0
  16. package/src/generated/PinescriptLexer.interp +267 -0
  17. package/src/generated/PinescriptLexer.tokens +131 -0
  18. package/src/generated/PinescriptLexer.ts +673 -0
  19. package/src/generated/PinescriptLexerBase.ts +395 -0
  20. package/src/generated/PinescriptParser.interp +267 -0
  21. package/src/generated/PinescriptParser.tokens +131 -0
  22. package/src/generated/PinescriptParser.ts +8863 -0
  23. package/src/generated/PinescriptParserBase.ts +21 -0
  24. package/src/generated/PinescriptParserVisitor.ts +840 -0
  25. package/src/index.ts +55 -0
  26. package/src/parser/LexerBase.ts +395 -0
  27. package/src/parser/ParserBase.ts +21 -0
  28. package/src/parser/error.ts +33 -0
  29. package/src/runtime/array.ts +486 -0
  30. package/src/runtime/color.ts +164 -0
  31. package/src/runtime/drawings.ts +510 -0
  32. package/src/runtime/input.ts +102 -0
  33. package/src/runtime/interpret.ts +3897 -0
  34. package/src/runtime/library.ts +282 -0
  35. package/src/runtime/log.ts +69 -0
  36. package/src/runtime/map.ts +85 -0
  37. package/src/runtime/math.ts +231 -0
  38. package/src/runtime/matrix.ts +1154 -0
  39. package/src/runtime/provider.ts +232 -0
  40. package/src/runtime/request.ts +372 -0
  41. package/src/runtime/series.ts +44 -0
  42. package/src/runtime/str.ts +433 -0
  43. package/src/runtime/strategy.ts +1105 -0
  44. package/src/runtime/ta.ts +2093 -0
  45. package/src/runtime/ticker.ts +81 -0
  46. package/src/runtime/time.ts +439 -0
  47. package/src/runtime/timeframe.ts +85 -0
  48. package/src/runtime/udt.ts +119 -0
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Copyright (C) 2024-2026 jango_blockchained
3
+ * SPDX-License-Identifier: AGPL-3.0-or-later
4
+ *
5
+ * Rich-inspired terminal renderer (PYNE volt palette).
6
+ * Enabled on TTY unless NO_COLOR / --plain; force with --rich or FORCE_COLOR.
7
+ * `--plain` always wins over leftover FORCE_COLOR.
8
+ */
9
+ const ACCENT = [183, 239, 9] as const;
10
+ const FG = [239, 239, 232] as const;
11
+ const MUTED = [203, 204, 189] as const;
12
+ const FAIL = [231, 0, 11] as const;
13
+ const WARN = [235, 169, 65] as const;
14
+
15
+ export const VERSION = "0.1.0";
16
+
17
+ type Rgb = readonly [number, number, number];
18
+
19
+ const ANSI_CSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
20
+
21
+ function fg(rgb: Rgb, text: string): string {
22
+ return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${text}\x1b[0m`;
23
+ }
24
+
25
+ function bold(text: string): string {
26
+ return `\x1b[1m${text}\x1b[0m`;
27
+ }
28
+
29
+ function envOn(name: string): boolean {
30
+ const v = process.env[name];
31
+ return v != null && v !== "" && v !== "0";
32
+ }
33
+
34
+ function hasFlag(argv: string[], flag: string): boolean {
35
+ for (const a of argv) {
36
+ if (a === flag) return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /** `--plain` / `--no-color` beat leftover FORCE_COLOR. `--rich` after the command is enough. */
42
+ export function useRich(argv: string[]): boolean {
43
+ if (hasFlag(argv, "--plain") || hasFlag(argv, "--no-color")) return false;
44
+ if (hasFlag(argv, "--rich")) return true;
45
+ if (envOn("NO_COLOR")) return false;
46
+ if (envOn("FORCE_COLOR")) return true;
47
+ return Boolean(process.stdout.isTTY);
48
+ }
49
+
50
+ /** Strip color-mode flags so they can sit after the command (bun will not see them first). */
51
+ export function stripFlags(argv: string[]): string[] {
52
+ const out: string[] = [];
53
+ for (const a of argv) {
54
+ if (a === "--plain" || a === "--no-color" || a === "--rich") continue;
55
+ out.push(a);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ export function termColumns(stream: { columns?: number } = process.stdout): number {
61
+ const c = stream.columns;
62
+ if (typeof c !== "number" || !Number.isFinite(c) || c < 1) return 80;
63
+ return Math.trunc(c);
64
+ }
65
+
66
+ export class Rich {
67
+ constructor(readonly enabled: boolean) {}
68
+
69
+ paint(kind: "accent" | "ok" | "fail" | "warn" | "muted" | "fg", text: string): string {
70
+ if (!this.enabled) return text;
71
+ const map: Record<string, Rgb> = {
72
+ accent: ACCENT,
73
+ ok: ACCENT,
74
+ fail: FAIL,
75
+ warn: WARN,
76
+ muted: MUTED,
77
+ fg: FG,
78
+ };
79
+ const painted = fg(map[kind] ?? FG, text);
80
+ return kind === "accent" || kind === "ok" || kind === "fail" ? bold(painted) : painted;
81
+ }
82
+
83
+ write(text: string, err = false): void {
84
+ const stream = err ? process.stderr : process.stdout;
85
+ stream.write(text.endsWith("\n") ? text : `${text}\n`);
86
+ }
87
+
88
+ status(kind: "ok" | "fail" | "warn" | "info", msg: string, err = false): void {
89
+ if (!this.enabled) {
90
+ const prefix = { ok: "OK", fail: "FAIL", warn: "WARN", info: "INFO" }[kind];
91
+ this.write(`${prefix}: ${msg}`, err);
92
+ return;
93
+ }
94
+ const mark = { ok: "✔", fail: "✘", warn: "!", info: "◆" }[kind];
95
+ const style = { ok: "ok", fail: "fail", warn: "warn", info: "accent" }[kind] as
96
+ | "ok"
97
+ | "fail"
98
+ | "warn"
99
+ | "accent";
100
+ this.write(`${this.paint(style, mark)} ${this.paint("fg", msg)}`, err);
101
+ }
102
+
103
+ banner(): void {
104
+ if (!this.enabled) {
105
+ this.write(`PYNE pynets v${VERSION}`);
106
+ this.write("TypeScript Pine toolchain — parse · format · run");
107
+ return;
108
+ }
109
+ this.write(
110
+ `${this.paint("accent", "◆ PYNE")} ${this.paint("fg", "pynets")} ${this.paint("muted", `v${VERSION}`)}`,
111
+ );
112
+ this.write(this.paint("muted", "TypeScript Pine toolchain — parse · format · run"));
113
+ }
114
+
115
+ help(): void {
116
+ this.banner();
117
+ this.write("");
118
+ if (this.enabled) {
119
+ this.write(this.paint("accent", "USAGE"));
120
+ this.write(` ${this.paint("fg", "pynets")} ${this.paint("muted", "<command> [file] [options]")}`);
121
+ this.write("");
122
+ this.write(this.paint("accent", "COMMANDS"));
123
+ } else {
124
+ this.write(
125
+ "usage: pynets <check|format|run|dump|info> [file] [--bars N] [--commission N] [--slippage N] [--pyramiding N] [--json] [--plain] [--rich] [--full]",
126
+ );
127
+ this.write("");
128
+ this.write("commands:");
129
+ }
130
+ const cmds: Array<[string, string]> = [
131
+ ["check", "Parse-only validation (CI-friendly exit codes)"],
132
+ ["format", "Parse → unparse (pretty-print on TTY)"],
133
+ ["run", "Interpret on synthetic OHLCV"],
134
+ ["dump", "Dump the ASDL-shaped AST"],
135
+ ["info", "Version and runtime extras"],
136
+ ];
137
+ for (const [name, desc] of cmds) {
138
+ if (this.enabled) {
139
+ this.write(` ${this.paint("ok", name.padEnd(10))} ${this.paint("muted", desc)}`);
140
+ } else {
141
+ this.write(` ${name.padEnd(10)} ${desc}`);
142
+ }
143
+ }
144
+ this.write("");
145
+ if (this.enabled) this.write(this.paint("accent", "OPTIONS"));
146
+ else this.write("options:");
147
+ const opts: Array<[string, string]> = [
148
+ ["--bars N", "Synthetic bar count for run (default 20, max 100000)"],
149
+ ["--commission N", "Broker commission fraction for run (e.g. 0.001)"],
150
+ ["--slippage N", "Broker slippage in price units for run"],
151
+ ["--pyramiding N", "Broker max same-direction adds for run"],
152
+ ["--json", "Machine-readable run / info output"],
153
+ ["--indent N", "AST dump indent (default 2)"],
154
+ ["--full", "Print the full AST dump (no 80-line cap)"],
155
+ ["--plain", "Disable Rich styling (wins over FORCE_COLOR)"],
156
+ ["--rich", "Force Rich styling (put after the command)"],
157
+ ["-h, --help", "Show this help"],
158
+ ];
159
+ const optPad = 16;
160
+ for (const [name, desc] of opts) {
161
+ if (this.enabled) {
162
+ this.write(` ${this.paint("fg", name.padEnd(optPad))} ${this.paint("muted", desc)}`);
163
+ } else {
164
+ this.write(` ${name.padEnd(optPad)} ${desc}`);
165
+ }
166
+ }
167
+ this.write("");
168
+ this.write(this.enabled ? this.paint("muted", "exit 0 ok · 1 syntax/runtime · 2 usage") : "exit: 0 ok, 1 syntax/runtime, 2 usage");
169
+ this.write("");
170
+ if (this.enabled) this.write(this.paint("accent", "EXAMPLES"));
171
+ else this.write("examples:");
172
+ const examples: Array<[string, string]> = [
173
+ ["pynets run keltner.pine --bars 40", "3 plots (mid / up / lo) + spark/last"],
174
+ ["pynets run strategy_entry.pine", "strategy events table (bar, type, id)"],
175
+ ["pynets run strat.pine --commission 0.001 --slippage 1 --pyramiding 0", "broker flags on Runtime"],
176
+ ["pynets dump script.pine --rich --full", "--rich after the command (bun will not eat it)"],
177
+ ];
178
+ for (const [cmd, desc] of examples) {
179
+ if (this.enabled) {
180
+ this.write(` ${this.paint("fg", cmd)}`);
181
+ this.write(` ${this.paint("muted", desc)}`);
182
+ } else {
183
+ this.write(` ${cmd}`);
184
+ this.write(` ${desc}`);
185
+ }
186
+ }
187
+ }
188
+
189
+ panel(title: string, lines: string[]): void {
190
+ const safeLines = Array.isArray(lines) ? lines : [];
191
+ if (!this.enabled) {
192
+ this.write(title);
193
+ for (const line of safeLines) this.write(` ${line}`);
194
+ return;
195
+ }
196
+ const body = safeLines.map((l) => visibleWidth(l ?? ""));
197
+ const cols = termColumns();
198
+ const maxInner = Math.max(4, cols - 2);
199
+ const natural = Math.max(visibleWidth(title) + 2, 0, ...body);
200
+ const inner = Math.min(maxInner, Math.max(natural, Math.min(28, maxInner)));
201
+ const dash = Math.max(1, inner - visibleWidth(title) - 1);
202
+ const top = `${this.paint("muted", "┌─")} ${this.paint("accent", title)} ${this.paint(
203
+ "muted",
204
+ "─".repeat(dash) + "┐",
205
+ )}`;
206
+ this.write(top);
207
+ for (const line of safeLines) {
208
+ const pad = " ".repeat(Math.max(0, inner - visibleWidth(line ?? "")));
209
+ this.write(`${this.paint("muted", "│")} ${line ?? ""}${pad}${this.paint("muted", "│")}`);
210
+ }
211
+ this.write(this.paint("muted", `└${"─".repeat(inner + 2)}┘`));
212
+ }
213
+
214
+ table(headers: string[], rows: string[][]): void {
215
+ const heads = Array.isArray(headers) ? headers : [];
216
+ const body = Array.isArray(rows) ? rows : [];
217
+ const widths = heads.map((h, i) =>
218
+ Math.max(visibleWidth(h ?? ""), ...body.map((r) => visibleWidth((r && r[i]) || ""))),
219
+ );
220
+ if (!this.enabled) {
221
+ this.write(heads.map((h, i) => (h ?? "").padEnd(widths[i] ?? 0)).join(" "));
222
+ for (const row of body) {
223
+ this.write((row ?? []).map((c, i) => padVisible(c ?? "", widths[i] ?? 0)).join(" "));
224
+ }
225
+ return;
226
+ }
227
+ this.write(
228
+ heads
229
+ .map((h, i) => this.paint("accent", (h ?? "").padEnd(widths[i] ?? 0)))
230
+ .join(this.paint("muted", " ")),
231
+ );
232
+ this.write(this.paint("muted", widths.map((w) => "─".repeat(w)).join("──")));
233
+ for (const row of body) {
234
+ this.write((row ?? []).map((c, i) => padVisible(c ?? "", widths[i] ?? 0)).join(" "));
235
+ }
236
+ }
237
+ }
238
+
239
+ function saneSparkWidth(width: number): number {
240
+ if (!Number.isFinite(width) || width < 0) return 16;
241
+ return Math.min(256, Math.trunc(width));
242
+ }
243
+
244
+ export function sparkline(values: Array<number | null>, width = 16): string {
245
+ const w = saneSparkWidth(width);
246
+ if (w === 0) return "";
247
+ const series = Array.isArray(values) ? values : [];
248
+ const nums = series.filter((v): v is number => v != null && Number.isFinite(v));
249
+ if (nums.length === 0) {
250
+ const n = series.length;
251
+ return "·".repeat(n === 0 ? Math.min(w, 8) : Math.min(w, n));
252
+ }
253
+ const lo = Math.min(...nums);
254
+ const hi = Math.max(...nums);
255
+ const span = hi - lo || 1;
256
+ const n = series.length;
257
+ const cols = Math.min(w, Math.max(1, n));
258
+ const blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
259
+ let out = "";
260
+ for (let c = 0; c < cols; c++) {
261
+ const i = cols === 1 ? n - 1 : Math.round((c * (n - 1)) / (cols - 1));
262
+ const v = series[i];
263
+ if (v == null || !Number.isFinite(v)) {
264
+ out += "·";
265
+ continue;
266
+ }
267
+ const idx = Math.min(blocks.length - 1, Math.floor(((v - lo) / span) * (blocks.length - 1)));
268
+ out += blocks[idx];
269
+ }
270
+ return out;
271
+ }
272
+
273
+ const PINE_KW =
274
+ /^(indicator|strategy|library|plot|input|var|varip|if|else|for|to|in|while|switch|export|import|true|false|na|and|or|not)$/;
275
+ const PINE_BI = /^(open|high|low|close|volume|time|bar_index|ta|sma|ema|rsi)$/;
276
+
277
+ export function highlightPine(source: string, rich: Rich): string {
278
+ if (!rich.enabled) return source;
279
+ return String(source ?? "")
280
+ .split("\n")
281
+ .map((line) => highlightLine(line, rich))
282
+ .join("\n");
283
+ }
284
+
285
+ function highlightLine(line: string, rich: Rich): string {
286
+ if (line.trimStart().startsWith("//")) return rich.paint("muted", line);
287
+ let out = "";
288
+ let i = 0;
289
+ while (i < line.length) {
290
+ const ch = line[i]!;
291
+ if (ch === "\x1b") {
292
+ const j = skipAnsi(line, i);
293
+ out += line.slice(i, j);
294
+ i = j;
295
+ continue;
296
+ }
297
+ if (ch === '"' || ch === "'") {
298
+ const q = ch;
299
+ let j = i + 1;
300
+ while (j < line.length && line[j] !== q) {
301
+ if (line[j] === "\\") j += 2;
302
+ else j++;
303
+ }
304
+ if (j < line.length) j++;
305
+ out += rich.paint("warn", line.slice(i, j));
306
+ i = j;
307
+ continue;
308
+ }
309
+ if (ch === "/" && line[i + 1] === "/") {
310
+ out += rich.paint("muted", line.slice(i));
311
+ break;
312
+ }
313
+ if (/[A-Za-z_]/.test(ch)) {
314
+ let j = i + 1;
315
+ while (j < line.length && /[A-Za-z0-9_]/.test(line[j]!)) j++;
316
+ while (line[j] === "." && /[A-Za-z_]/.test(line[j + 1] ?? "")) {
317
+ j++;
318
+ while (j < line.length && /[A-Za-z0-9_]/.test(line[j]!)) j++;
319
+ }
320
+ const word = line.slice(i, j);
321
+ if (PINE_KW.test(word)) out += rich.paint("accent", word);
322
+ else if (PINE_BI.test(word) || PINE_BI.test(word.split(".").pop() ?? "")) out += rich.paint("ok", word);
323
+ else out += rich.paint("fg", word);
324
+ i = j;
325
+ continue;
326
+ }
327
+ if (/[0-9]/.test(ch)) {
328
+ const j = scanNumber(line, i);
329
+ out += rich.paint("warn", line.slice(i, j));
330
+ i = j;
331
+ continue;
332
+ }
333
+ out += ch;
334
+ i++;
335
+ }
336
+ return out;
337
+ }
338
+
339
+ /** Skip one CSI / simple ESC sequence so highlighters do not recode its digits. */
340
+ export function skipAnsi(text: string, from: number): number {
341
+ if (text[from] !== "\x1b") return from + 1;
342
+ let j = from + 1;
343
+ if (text[j] === "[") {
344
+ j++;
345
+ while (j < text.length && /[0-9;:?]/.test(text[j]!)) j++;
346
+ if (j < text.length && /[A-Za-z]/.test(text[j]!)) j++;
347
+ return j;
348
+ }
349
+ if (j < text.length) return j + 1;
350
+ return j;
351
+ }
352
+
353
+ function scanNumber(line: string, i: number): number {
354
+ let j = i;
355
+ while (j < line.length && /[0-9]/.test(line[j]!)) j++;
356
+ if (line[j] === "." && /[0-9]/.test(line[j + 1] ?? "")) {
357
+ j++;
358
+ while (j < line.length && /[0-9]/.test(line[j]!)) j++;
359
+ }
360
+ if (line[j] === "e" || line[j] === "E") {
361
+ let k = j + 1;
362
+ if (line[k] === "+" || line[k] === "-") k++;
363
+ if (/[0-9]/.test(line[k] ?? "")) {
364
+ j = k;
365
+ while (j < line.length && /[0-9]/.test(line[j]!)) j++;
366
+ }
367
+ }
368
+ return j;
369
+ }
370
+
371
+ export function visibleWidth(text: string): number {
372
+ return String(text ?? "").replace(ANSI_CSI, "").length;
373
+ }
374
+
375
+ function padVisible(text: string, width: number): string {
376
+ const extra = width - visibleWidth(text);
377
+ return extra > 0 ? text + " ".repeat(extra) : text;
378
+ }