@hoox-sh/pynets 0.2.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.
@@ -8,6 +8,7 @@ import { parse } from "../../ast/helper.ts";
8
8
  import type {
9
9
  Assign,
10
10
  Attribute,
11
+ AugAssign,
11
12
  BinOp,
12
13
  BoolOp,
13
14
  Call,
@@ -23,6 +24,7 @@ import type {
23
24
  If,
24
25
  Import,
25
26
  Name,
27
+ Once,
26
28
  ReAssign,
27
29
  Script,
28
30
  stmt,
@@ -33,7 +35,7 @@ import type {
33
35
  UnaryOp,
34
36
  While,
35
37
  } from "../../ast/nodes.ts";
36
- import { emitCall } from "./emit_call.ts";
38
+ import { emitCall, TA_BARE_ATTRS } from "./emit_call.ts";
37
39
  import { emitFunctionDef } from "./emit_udf.ts";
38
40
  import type { EmitCtx, VisitFn } from "./types.ts";
39
41
 
@@ -47,6 +49,7 @@ const CHART_LOCALS = new Set([
47
49
  "close",
48
50
  "volume",
49
51
  "time",
52
+ "time_close",
50
53
  "bar_index",
51
54
  "last_bar_index",
52
55
  "na",
@@ -138,6 +141,7 @@ export function newEmitCtx(): EmitCtx {
138
141
  usesStrategy: false,
139
142
  nextSite: 0,
140
143
  errors: [],
144
+ onceFlags: [],
141
145
  currentFunc: null,
142
146
  currentParamNames: new Set(),
143
147
  currentSeriesParams: new Set(),
@@ -210,6 +214,9 @@ function assemble(ctx: EmitCtx, body: string[]): string {
210
214
  lines.push(` const ${arr} = new Array(n_bars).fill(null);`);
211
215
  }
212
216
  lines.push(" const __var_inited = Object.create(null);");
217
+ for (const flag of ctx.onceFlags) {
218
+ lines.push(` let ${flag} = false;`);
219
+ }
213
220
  for (const fn of ctx.functions) {
214
221
  if (fn) lines.push(indentBlock(fn, 2));
215
222
  }
@@ -223,6 +230,7 @@ function assemble(ctx: EmitCtx, body: string[]): string {
223
230
  lines.push(" const close = close_arr[__bar_idx];");
224
231
  lines.push(" const volume = vol_arr[__bar_idx];");
225
232
  lines.push(" const time = time_arr[__bar_idx];");
233
+ lines.push(" const time_close = __h.timeClose(__bar_idx, time_arr);");
226
234
  lines.push(" const bar_index = __bar_idx;");
227
235
  lines.push(" const last_bar_index = n_bars - 1;");
228
236
  lines.push(" const na = null;");
@@ -267,6 +275,7 @@ function emitNode(state: State, node: expr | stmt | null | undefined): string {
267
275
  switch (node.kind) {
268
276
  case "Assign":
269
277
  case "ReAssign":
278
+ case "AugAssign":
270
279
  case "FunctionDef":
271
280
  case "TypeDef":
272
281
  case "EnumDef":
@@ -274,6 +283,7 @@ function emitNode(state: State, node: expr | stmt | null | undefined): string {
274
283
  case "Break":
275
284
  case "Continue":
276
285
  case "Expr":
286
+ case "Once":
277
287
  return emitStmt(state, node);
278
288
  default:
279
289
  return emitExpr(state, node);
@@ -288,6 +298,8 @@ function emitStmt(state: State, node: stmt | expr): string {
288
298
  return emitAssign(state, node);
289
299
  case "ReAssign":
290
300
  return emitReAssign(state, node);
301
+ case "AugAssign":
302
+ return emitAugAssign(state, node);
291
303
  case "FunctionDef":
292
304
  emitFunctionDef(state.ctx, node as FunctionDef, (s) => emitStmt(state, s));
293
305
  if (state.ctx.currentFunc != null) state.ctx.currentFunc = null;
@@ -312,6 +324,8 @@ function emitStmt(state: State, node: stmt | expr): string {
312
324
  return emitWhile(state, node);
313
325
  case "Switch":
314
326
  return emitSwitch(state, node, false);
327
+ case "Once":
328
+ return emitOnce(state, node);
315
329
  default:
316
330
  return emitExprStmt(state, node);
317
331
  }
@@ -330,6 +344,8 @@ function emitExprStmt(state: State, value: expr | null | undefined): string {
330
344
  return emitWhile(state, value);
331
345
  case "Switch":
332
346
  return emitSwitch(state, value, false);
347
+ case "Once":
348
+ return emitOnce(state, value);
333
349
  default: {
334
350
  const e = emitExpr(state, value);
335
351
  if (!e || e === "null") return "";
@@ -373,6 +389,12 @@ function emitExpr(state: State, node: expr | null | undefined): string {
373
389
  return iife(emitForIn(state, node), "null");
374
390
  case "While":
375
391
  return iife(emitWhile(state, node), "null");
392
+ case "Qualify":
393
+ return emitExpr(state, node.value);
394
+ case "Specialize":
395
+ return emitExpr(state, node.value);
396
+ case "AugAssign":
397
+ return emitAugAssign(state, node);
376
398
  default:
377
399
  return "null";
378
400
  }
@@ -423,11 +445,25 @@ function emitBinOp(state: State, node: BinOp): string {
423
445
  return `__h.div(${left}, ${right})`;
424
446
  case "Mod":
425
447
  return `__h.mod(${left}, ${right})`;
448
+ case "BitAnd":
449
+ return emitBitOp(left, right, "&");
450
+ case "BitOr":
451
+ return emitBitOp(left, right, "|");
452
+ case "BitXor":
453
+ return emitBitOp(left, right, "^");
454
+ case "LShift":
455
+ return emitBitOp(left, right, "<<");
456
+ case "RShift":
457
+ return emitBitOp(left, right, ">>");
426
458
  default:
427
459
  return `__h.add(${left}, ${right})`;
428
460
  }
429
461
  }
430
462
 
463
+ function emitBitOp(left: string, right: string, op: "&" | "|" | "^" | "<<" | ">>"): string {
464
+ return `((__l, __r) => { if (__h.isNa(__l) || __h.isNa(__r)) return null; return (__h.nz(__l) | 0) ${op} (__h.nz(__r) | 0); })(${left}, ${right})`;
465
+ }
466
+
431
467
  function emitUnaryOp(state: State, node: UnaryOp): string {
432
468
  const operand = emitExpr(state, node.operand);
433
469
  switch (opKind(node.op)) {
@@ -500,9 +536,13 @@ const STRATEGY_QUERY_ATTRS = new Set([
500
536
  "openprofit",
501
537
  "opentrades",
502
538
  "closedtrades",
539
+ "leverage",
540
+ "margin_liquidation_price",
541
+ "position_avg_price",
542
+ "initial_capital",
503
543
  ]);
504
544
 
505
- /** Namespaces used only as call receivers — not UDT field access. */
545
+ /** Namespaces used as call receivers or dotted constants — not UDT field access. */
506
546
  const ATTR_NAMESPACES = new Set([
507
547
  "ta",
508
548
  "math",
@@ -521,8 +561,150 @@ const ATTR_NAMESPACES = new Set([
521
561
  "session",
522
562
  "chart",
523
563
  "log",
564
+ "order",
565
+ "format",
566
+ "text",
567
+ "size",
568
+ "barmerge",
569
+ "shape",
570
+ "location",
571
+ "xloc",
572
+ "yloc",
573
+ "extend",
574
+ "display",
575
+ "position",
576
+ "hline",
577
+ "dayofweek",
578
+ "month",
579
+ ]);
580
+
581
+ /** Dotted keys from Python `_MATH_CONSTANTS` (base.py). Unknown member → na. */
582
+ const ATTR_CONSTANTS: Record<string, string | number | boolean> = {
583
+ // Math constants — Python SoT `_MATH_CONSTANTS` (evaluator/base.py).
584
+ "math.pi": Math.PI,
585
+ "math.e": Math.E,
586
+ "math.phi": (1 + Math.sqrt(5)) / 2,
587
+ "math.rphi": 2 / (1 + Math.sqrt(5)),
588
+ "format.mintick": "mintick",
589
+ "format.percent": "percent",
590
+ "format.volume": "volume",
591
+ "format.price": "price",
592
+ "text.formatting.none": "",
593
+ "text.formatting.bold": "bold",
594
+ "text.formatting.italic": "italic",
595
+ "text.formatting.bold_italic": "bold italic",
596
+ "size.auto": "auto",
597
+ "size.tiny": 8,
598
+ "size.small": 10,
599
+ "size.normal": 12,
600
+ "size.large": 16,
601
+ "size.huge": 20,
602
+ "order.ascending": 1,
603
+ "order.descending": -1,
604
+ "barmerge.gaps_on": true,
605
+ "barmerge.gaps_off": false,
606
+ "barmerge.lookahead_on": true,
607
+ "barmerge.lookahead_off": false,
608
+ "shape.arrowup": "arrowup",
609
+ "shape.arrowdown": "arrowdown",
610
+ "shape.circle": "circle",
611
+ "shape.cross": "cross",
612
+ "shape.diamond": "diamond",
613
+ "shape.flag": "flag",
614
+ "shape.labelup": "labelup",
615
+ "shape.labeldown": "labeldown",
616
+ "shape.square": "square",
617
+ "shape.triangledown": "triangledown",
618
+ "shape.triangleup": "triangleup",
619
+ "shape.xcross": "xcross",
620
+ "location.abovebar": "abovebar",
621
+ "location.belowbar": "belowbar",
622
+ "location.top": "top",
623
+ "location.bottom": "bottom",
624
+ "location.absolute": "absolute",
625
+ "xloc.bar_index": "bar_index",
626
+ "xloc.bar_time": "bar_time",
627
+ "yloc.price": "price",
628
+ "yloc.abovebar": "abovebar",
629
+ "yloc.belowbar": "belowbar",
630
+ "extend.none": "none",
631
+ "extend.left": "left",
632
+ "extend.right": "right",
633
+ "extend.both": "both",
634
+ // Pine display.* is a bitfield: none=0 pane=1 data_window=2 price_scale=4
635
+ // status_line=8 all=15. Integers so `display.pane + display.data_window` works
636
+ // (Python SoT base.py / compiler.py — matches interpret backend).
637
+ "display.none": 0,
638
+ "display.pane": 1,
639
+ "display.all": 15,
640
+ "display.data_window": 2,
641
+ "display.price_scale": 4,
642
+ "display.status_line": 8,
643
+ "position.top_left": "top_left",
644
+ "position.top_center": "top_center",
645
+ "position.top_right": "top_right",
646
+ "position.middle_left": "middle_left",
647
+ "position.middle_center": "middle_center",
648
+ "position.middle_right": "middle_right",
649
+ "position.bottom_left": "bottom_left",
650
+ "position.bottom_center": "bottom_center",
651
+ "position.bottom_right": "bottom_right",
652
+ "hline.style_solid": "solid",
653
+ "hline.style_dashed": "dashed",
654
+ "hline.style_dotted": "dotted",
655
+ "dayofweek.sunday": 1,
656
+ "dayofweek.monday": 2,
657
+ "dayofweek.tuesday": 3,
658
+ "dayofweek.wednesday": 4,
659
+ "dayofweek.thursday": 5,
660
+ "dayofweek.friday": 6,
661
+ "dayofweek.saturday": 7,
662
+ "month.january": 1,
663
+ "month.february": 2,
664
+ "month.march": 3,
665
+ "month.april": 4,
666
+ "month.may": 5,
667
+ "month.june": 6,
668
+ "month.july": 7,
669
+ "month.august": 8,
670
+ "month.september": 9,
671
+ "month.october": 10,
672
+ "month.november": 11,
673
+ "month.december": 12,
674
+ };
675
+
676
+ const ATTR_CONST_NS = new Set([
677
+ "math",
678
+ "order",
679
+ "format",
680
+ "text",
681
+ "size",
682
+ "barmerge",
683
+ "shape",
684
+ "location",
685
+ "xloc",
686
+ "yloc",
687
+ "extend",
688
+ "display",
689
+ "position",
690
+ "hline",
691
+ "dayofweek",
692
+ "month",
524
693
  ]);
525
694
 
695
+ function attrQualifiedName(node: Attribute): string | null {
696
+ const parts: string[] = [node.attr];
697
+ let cur: expr = node.value;
698
+ while (cur.kind === "Attribute") {
699
+ parts.push(cur.attr);
700
+ cur = cur.value;
701
+ }
702
+ if (cur.kind !== "Name") return null;
703
+ parts.push(cur.id);
704
+ parts.reverse();
705
+ return parts.join(".");
706
+ }
707
+
526
708
  function emitBarstate(attr: string): string {
527
709
  switch (attr) {
528
710
  case "isfirst":
@@ -584,19 +766,34 @@ function emitChart(attr: string): string {
584
766
  }
585
767
  }
586
768
 
769
+ // Static defaults mirror the Python SoT flat table in
770
+ // pynescript/ast/evaluator/base.py ("Chart timeframe defaults (daily)"),
771
+ // cross-checked with runtime/host.py Timeframe (same values). The compile host
772
+ // has no timeframe context (`__h.timeframe` is undefined), so these are the
773
+ // effective values; interpret derives them dynamically from env.timeframe and
774
+ // resolves to the same daily table when no timeframe is configured — every
775
+ // timeframe.* member is backend-parity (commit 6977a42; pinned in
776
+ // test/compile_timeframe_defaults.test.ts).
587
777
  function emitTimeframe(attr: string): string {
588
778
  switch (attr) {
589
779
  case "period":
590
- return `(__h.timeframe || "1")`;
780
+ case "main_period":
781
+ return `(__h.timeframe || "D")`;
591
782
  case "multiplier":
592
783
  return "1";
593
784
  case "isintraday":
594
- return "true";
595
- case "isdaily":
596
785
  case "isweekly":
597
786
  case "ismonthly":
787
+ case "isseconds":
788
+ case "isinseconds":
789
+ case "isminutes":
790
+ case "ishours":
598
791
  return "false";
792
+ case "isdaily":
793
+ case "isdwm":
794
+ return "true";
599
795
  default:
796
+ // Unknown member → na (documented behavior).
600
797
  return "null";
601
798
  }
602
799
  }
@@ -607,6 +804,8 @@ function emitSyminfo(attr: string): string {
607
804
  case "tickerid":
608
805
  case "root":
609
806
  return `"SYMBOL"`;
807
+ case "prefix":
808
+ return `__h.syminfoPrefix()`;
610
809
  default:
611
810
  return "null";
612
811
  }
@@ -635,10 +834,22 @@ function emitAttribute(state: State, node: Attribute): string {
635
834
  if (node.value.kind === "Name" && node.value.id === "color") {
636
835
  return `__h.colorByName(${JSON.stringify(node.attr)})`;
637
836
  }
837
+ if (node.value.kind === "Name" && node.value.id === "ta") {
838
+ // Bare `ta.<attr>` auto-calls only for the whitelist interpret's
839
+ // evalTaAttr supports; everything else is na on both backends.
840
+ if (!TA_BARE_ATTRS.has(node.attr)) return "null";
841
+ return emitCall(state.ctx, { kind: "Call", func: node, args: [] }, visitOf(state));
842
+ }
638
843
  if (node.value.kind === "Name" && node.value.id === "strategy") {
639
844
  const attr = node.attr;
640
845
  if (attr === "long") return `"long"`;
641
846
  if (attr === "short") return `"short"`;
847
+ if (attr === "avg_price_stock") return `"stock"`;
848
+ if (attr === "avg_price_futures") return `"futures"`;
849
+ if (attr === "avg_price_inverse") return `"inverse"`;
850
+ if (attr === "cash") return `"cash"`;
851
+ if (attr === "fixed") return `"fixed"`;
852
+ if (attr === "percent_of_equity") return `"percent_of_equity"`;
642
853
  if (STRATEGY_QUERY_ATTRS.has(attr)) {
643
854
  state.ctx.usesStrategy = true;
644
855
  return `__h.strategy.${attr}()`;
@@ -650,6 +861,15 @@ function emitAttribute(state: State, node: Attribute): string {
650
861
  return JSON.stringify(node.attr);
651
862
  }
652
863
  }
864
+ const qn = attrQualifiedName(node);
865
+ if (qn != null) {
866
+ const dot = qn.indexOf(".");
867
+ const root = dot < 0 ? qn : qn.slice(0, dot);
868
+ if (ATTR_CONST_NS.has(root)) {
869
+ const lit = ATTR_CONSTANTS[qn];
870
+ return lit === undefined ? "null" : JSON.stringify(lit);
871
+ }
872
+ }
653
873
  const obj = emitExpr(state, node.value);
654
874
  if (node.value.kind === "Name" && state.ctx.udtTypes.has(node.value.id)) {
655
875
  return `__h.udtGet(${obj}, ${JSON.stringify(node.attr)})`;
@@ -772,6 +992,21 @@ function emitReAssign(state: State, node: ReAssign): string {
772
992
  return emitStore(state, node.target, emitExpr(state, node.value), false);
773
993
  }
774
994
 
995
+ function emitAugAssign(state: State, node: AugAssign): string {
996
+ // Attribute targets store the raw rhs — the old field is discarded
997
+ // (quirk preserved from Python visit_AugAssign and interpret evalAugAssign).
998
+ if (node.target.kind === "Attribute") {
999
+ return emitStore(state, node.target, emitExpr(state, node.value), false);
1000
+ }
1001
+ const rhs = emitBinOp(state, {
1002
+ kind: "BinOp",
1003
+ left: node.target,
1004
+ op: node.op,
1005
+ right: node.value,
1006
+ });
1007
+ return emitStore(state, node.target, rhs, false);
1008
+ }
1009
+
775
1010
  function emitStore(state: State, target: expr, rhs: string, isVar: boolean): string {
776
1011
  if (target.kind === "Name") {
777
1012
  return storeName(state, target.id, rhs, isVar);
@@ -842,6 +1077,15 @@ function emitIfStmt(state: State, node: If, asExpr = false): string {
842
1077
  return `if (${test}) {\n${indentBlock(thenBlock, 2)}\n} else {\n${indentBlock(elseBlock, 2)}\n}`;
843
1078
  }
844
1079
 
1080
+ function emitOnce(state: State, node: Once): string {
1081
+ const flag = `__once_${state.ctx.onceFlags.length}`;
1082
+ state.ctx.onceFlags.push(flag);
1083
+ const test = node.test == null ? "true" : `__h.nz(${emitExpr(state, node.test)})`;
1084
+ const body = emitBranch(state, node.body, false);
1085
+ const inner = body ? `${indentBlock(body, 4)}\n ${flag} = true;` : ` ${flag} = true;`;
1086
+ return `if (!${flag}) {\n if (${test}) {\n${inner}\n }\n}`;
1087
+ }
1088
+
845
1089
  function emitBranch(state: State, stmts: stmt[], asExpr: boolean): string {
846
1090
  const list = stmts ?? [];
847
1091
  if (list.length === 0) return asExpr ? "return null;" : "";
@@ -900,6 +1144,7 @@ function isValueExpr(node: expr): boolean {
900
1144
  case "ForIn":
901
1145
  case "While":
902
1146
  case "Switch":
1147
+ case "Once":
903
1148
  return false;
904
1149
  default:
905
1150
  return true;
@@ -1021,10 +1266,7 @@ function emitSwitch(state: State, node: Switch, asExpr: boolean): string {
1021
1266
  defaultCase = c;
1022
1267
  continue;
1023
1268
  }
1024
- const cond =
1025
- subjN != null
1026
- ? `__h.eq(${subjN}, ${emitExpr(state, c.pattern)})`
1027
- : `__h.nz(${emitExpr(state, c.pattern)})`;
1269
+ const cond = emitSwitchCond(state, c.pattern, subjN);
1028
1270
  const kw = first ? "if" : "else if";
1029
1271
  first = false;
1030
1272
  const body = emitBranch(state, c.body ?? [], asExpr);
@@ -1045,6 +1287,21 @@ function emitSwitch(state: State, node: Switch, asExpr: boolean): string {
1045
1287
  return `{\n${indentBlock(block, 2)}\n}`;
1046
1288
  }
1047
1289
 
1290
+ /** Python `_switch_case_matches`: a comma arm (`1, 2 =>`) matches any element. */
1291
+ function emitSwitchCond(state: State, pattern: expr, subjN: string | null): string {
1292
+ if (pattern.kind === "Tuple") {
1293
+ const parts = (pattern.elts ?? []).map((e) => emitSwitchLeaf(state, e, subjN));
1294
+ return parts.length ? `(${parts.join(" || ")})` : "false";
1295
+ }
1296
+ return emitSwitchLeaf(state, pattern, subjN);
1297
+ }
1298
+
1299
+ function emitSwitchLeaf(state: State, pattern: expr, subjN: string | null): string {
1300
+ return subjN != null
1301
+ ? `__h.eq(${subjN}, ${emitExpr(state, pattern)})`
1302
+ : `__h.nz(${emitExpr(state, pattern)})`;
1303
+ }
1304
+
1048
1305
  function emitBlock(state: State, stmts: stmt[] | null | undefined): string {
1049
1306
  const lines: string[] = [];
1050
1307
  for (const s of stmts ?? []) {