@hoox-sh/pynets 0.3.0 → 0.4.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.
@@ -418,6 +418,8 @@ function emitName(state: State, node: Name): string {
418
418
  if (CHART_LOCALS.has(id)) return id;
419
419
  if (state.ctx.userFuncs.has(id)) return id;
420
420
  if (state.ctx.enumTypes.has(id)) return JSON.stringify(id);
421
+ // Dataset last-bar time on every bar (interpret ctx.timenow / lastBarTime).
422
+ if (id === "timenow") return "__h.timenow(time_arr)";
421
423
  return safeIdent(id);
422
424
  }
423
425
 
@@ -540,8 +542,57 @@ const STRATEGY_QUERY_ATTRS = new Set([
540
542
  "margin_liquidation_price",
541
543
  "position_avg_price",
542
544
  "initial_capital",
545
+ "cash",
546
+ "position_entry_name",
547
+ "account_currency",
548
+ "default_entry_qty",
549
+ "max_contracts_held_all",
550
+ "max_contracts_held_long",
551
+ "max_contracts_held_short",
552
+ "wintrades",
553
+ "losstrades",
554
+ "eventrades",
555
+ "grossprofit",
556
+ "grossloss",
557
+ "avg_trade",
558
+ "avg_winning_trade",
559
+ "avg_losing_trade",
560
+ "netprofit_percent",
561
+ "openprofit_percent",
562
+ "grossprofit_percent",
563
+ "grossloss_percent",
564
+ "avg_trade_percent",
565
+ "avg_winning_trade_percent",
566
+ "avg_losing_trade_percent",
567
+ "max_drawdown",
568
+ "max_drawdown_percent",
569
+ "max_runup",
570
+ "max_runup_percent",
571
+ "percent_profitable",
572
+ "winrate",
573
+ "profitfactor",
574
+ "profit_factor",
543
575
  ]);
544
576
 
577
+ /** Nested `strategy.*` constants (interpret `strategySeriesValue`). */
578
+ const STRATEGY_NESTED_CONSTS: Record<string, string> = {
579
+ "strategy.direction.long": "long",
580
+ "strategy.direction.short": "short",
581
+ "strategy.direction.all": "all",
582
+ "strategy.oca.none": "none",
583
+ "strategy.oca.cancel": "cancel",
584
+ "strategy.oca.reduce": "reduce",
585
+ "strategy.commission.percent": "percent",
586
+ "strategy.commission.cash_per_order": "cash_per_order",
587
+ "strategy.commission.cash_per_contract": "cash_per_contract",
588
+ };
589
+
590
+ /** Nested `strategy.*` series that are not a single identifier. */
591
+ const STRATEGY_NESTED_CALLS: Record<string, string> = {
592
+ "strategy.closedtrades.first_index": "closedtrades_first_index",
593
+ "strategy.opentrades.capital_held": "capital_held",
594
+ };
595
+
545
596
  /** Namespaces used as call receivers or dotted constants — not UDT field access. */
546
597
  const ATTR_NAMESPACES = new Set([
547
598
  "ta",
@@ -652,6 +703,10 @@ const ATTR_CONSTANTS: Record<string, string | number | boolean> = {
652
703
  "hline.style_solid": "solid",
653
704
  "hline.style_dashed": "dashed",
654
705
  "hline.style_dotted": "dotted",
706
+ // Python plotting.py PlotStyle — longer than hline's "solid".
707
+ "plot.linestyle_solid": "linestyle_solid",
708
+ "plot.linestyle_dashed": "linestyle_dashed",
709
+ "plot.linestyle_dotted": "linestyle_dotted",
655
710
  "dayofweek.sunday": 1,
656
711
  "dayofweek.monday": 2,
657
712
  "dayofweek.tuesday": 3,
@@ -688,6 +743,7 @@ const ATTR_CONST_NS = new Set([
688
743
  "display",
689
744
  "position",
690
745
  "hline",
746
+ "plot",
691
747
  "dayofweek",
692
748
  "month",
693
749
  ]);
@@ -847,13 +903,29 @@ function emitAttribute(state: State, node: Attribute): string {
847
903
  if (attr === "avg_price_stock") return `"stock"`;
848
904
  if (attr === "avg_price_futures") return `"futures"`;
849
905
  if (attr === "avg_price_inverse") return `"inverse"`;
850
- if (attr === "cash") return `"cash"`;
851
906
  if (attr === "fixed") return `"fixed"`;
852
907
  if (attr === "percent_of_equity") return `"percent_of_equity"`;
908
+ // `strategy.commission` the series is 0; `strategy.commission.percent` is nested.
909
+ if (attr === "commission") return "0";
853
910
  if (STRATEGY_QUERY_ATTRS.has(attr)) {
854
911
  state.ctx.usesStrategy = true;
855
912
  return `__h.strategy.${attr}()`;
856
913
  }
914
+ return "null";
915
+ }
916
+ if (node.attr === "all" && node.value.kind === "Name") {
917
+ const kind = node.value.id;
918
+ if (
919
+ kind === "line" ||
920
+ kind === "label" ||
921
+ kind === "box" ||
922
+ kind === "table" ||
923
+ kind === "polyline" ||
924
+ kind === "linefill"
925
+ ) {
926
+ state.ctx.usesDrawings = true;
927
+ return `__h.draw.all(${JSON.stringify(kind)})`;
928
+ }
857
929
  }
858
930
  if (node.value.kind === "Name") {
859
931
  const members = state.ctx.enumTypes.get(node.value.id);
@@ -863,6 +935,14 @@ function emitAttribute(state: State, node: Attribute): string {
863
935
  }
864
936
  const qn = attrQualifiedName(node);
865
937
  if (qn != null) {
938
+ const nested = STRATEGY_NESTED_CONSTS[qn];
939
+ if (nested !== undefined) return JSON.stringify(nested);
940
+ const nestedCall = STRATEGY_NESTED_CALLS[qn];
941
+ if (nestedCall != null) {
942
+ state.ctx.usesStrategy = true;
943
+ return `__h.strategy.${nestedCall}()`;
944
+ }
945
+ if (qn.startsWith("strategy.") && qn.split(".").length > 2) return "null";
866
946
  const dot = qn.indexOf(".");
867
947
  const root = dot < 0 ? qn : qn.slice(0, dot);
868
948
  if (ATTR_CONST_NS.has(root)) {
@@ -136,6 +136,23 @@ const TA_METHODS = new Set([
136
136
  "fractal",
137
137
  "atr_stop",
138
138
  "zigzag",
139
+ "hammer",
140
+ "gap_detector",
141
+ "macd_signal",
142
+ "acceleration_factor",
143
+ "stoch_smooth",
144
+ "engulfing",
145
+ "inside_bar_pattern",
146
+ "double_top_bottom",
147
+ "voi",
148
+ "bid_ask_imbalance",
149
+ "rsi_oversold_overbought",
150
+ "rsi_divergence",
151
+ "cumulative_delta",
152
+ "volume_momentum",
153
+ "volume_profile_high",
154
+ "volume_profile_low",
155
+ "volume_thrust",
139
156
  ]);
140
157
 
141
158
  /**
@@ -367,7 +384,15 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
367
384
  return `(() => { throw new Error(String(${msg})); })()`;
368
385
  }
369
386
 
370
- if (name === "alertcondition") return "null";
387
+ if (name === "max_bars_back") return "null";
388
+ if (name === "timenow") return "__h.timenow(time_arr)";
389
+ if (name === "alertcondition") {
390
+ ctx.usesDrawings = true;
391
+ const cond = pick(args, 0, ["condition"], "null");
392
+ const title = hasArg(args, 1, ["title"]) ? pick(args, 1, ["title"], `"Alert"`) : `"Alert"`;
393
+ const message = hasArg(args, 2, ["message"]) ? pick(args, 2, ["message"], `"Alert"`) : `"Alert"`;
394
+ return `__h.draw.alertcondition(__bar_idx, ${cond}, ${title}, ${message})`;
395
+ }
371
396
  if (name === "str_tostring" || name === "tostring") {
372
397
  const x = pick(args, 0, ["value", "source", "x"], "null");
373
398
  return `String(${x} ?? "")`;
@@ -525,6 +550,19 @@ function finiteOr(expr: string, fb: string): string {
525
550
  return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
526
551
  }
527
552
 
553
+ /** Interpret `unwrap`: bool → 1/0, non-finite → null. */
554
+ function cellOf(expr: string): string {
555
+ return `((__c) => (__c == null ? null : typeof __c === "boolean" ? (__c ? 1 : 0) : (typeof __c === "number" && Number.isFinite(__c) ? __c : null)))(${expr})`;
556
+ }
557
+
558
+ /**
559
+ * Interpret `wave10bList`: a Pine array becomes its cells; a scalar is a
560
+ * one-element list. Bar-mode series stay one element.
561
+ */
562
+ function cellList(expr: string): string {
563
+ return `(() => { const __v = ${expr}; const __cell = (e) => (e == null ? null : typeof e === "boolean" ? (e ? 1 : 0) : (typeof e === "number" && Number.isFinite(e) ? e : null)); if (__v != null && typeof __v.size === "function" && typeof __v.get === "function") { const __n = __v.size(); const __o = []; for (let __i = 0; __i < __n; __i++) __o.push(__cell(__v.get(__i))); return __o; } return [__cell(__v)]; })()`;
564
+ }
565
+
528
566
  const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
529
567
 
530
568
  const ARRAY_SIGS: Record<string, string[][]> = {
@@ -704,6 +742,10 @@ function emitHostCall(ns: string, method: string, args: CallArgs, params: string
704
742
  function emitArray(name: string, args: CallArgs): string | null {
705
743
  const raw = nsMethod(name, "array_");
706
744
  if (raw == null) return null;
745
+ if (raw === "from") {
746
+ const parts = args.pos.length > 0 ? args.pos : Object.values(args.kw);
747
+ return `__h.array.from([${parts.join(", ")}])`;
748
+ }
707
749
  const method = ctorMethod(raw);
708
750
  const params = ARRAY_SIGS[method];
709
751
  if (params == null) return null;
@@ -745,11 +787,24 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
745
787
  label: {
746
788
  set_text: [["id"], ["text"]],
747
789
  set_xy: [["id"], ["x"], ["y"]],
790
+ set_x: [["id"], ["x"]],
791
+ set_y: [["id"], ["y"]],
748
792
  set_color: [["id"], ["color"]],
749
793
  set_style: [["id"], ["style"]],
750
794
  set_size: [["id"], ["size"]],
751
795
  set_tooltip: [["id"], ["tooltip"]],
752
796
  set_textalign: [["id"], ["textalign"]],
797
+ set_textcolor: [["id"], ["color", "textcolor"]],
798
+ set_text_font_family: [["id"], ["font_family", "text_font_family"]],
799
+ set_text_halign: [["id"], ["halign", "text_halign"]],
800
+ set_text_valign: [["id"], ["valign", "text_valign"]],
801
+ set_text_size: [["id"], ["size", "text_size"]],
802
+ set_text_formatting: [["id"], ["formatting", "text_formatting"]],
803
+ set_border_color: [["id"], ["color"]],
804
+ set_border_width: [["id"], ["width"]],
805
+ set_border_style: [["id"], ["style"]],
806
+ set_xloc: [["id"], ["xloc"]],
807
+ set_yloc: [["id"], ["yloc"]],
753
808
  delete: [["id"]],
754
809
  get_text: [["id"]],
755
810
  get_x: [["id"]],
@@ -757,12 +812,17 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
757
812
  },
758
813
  line: {
759
814
  set_xy: [["id"], ["x1"], ["y1"], ["x2"], ["y2"]],
760
- set_xy1: [["id"], ["x1"], ["y1"]],
761
- set_xy2: [["id"], ["x2"], ["y2"]],
815
+ set_xy1: [["id"], ["x1", "x"], ["y1", "y"]],
816
+ set_xy2: [["id"], ["x2", "x"], ["y2", "y"]],
817
+ set_x1: [["id"], ["x1", "x"]],
818
+ set_y1: [["id"], ["y1", "y"]],
819
+ set_x2: [["id"], ["x2", "x"]],
820
+ set_y2: [["id"], ["y2", "y"]],
762
821
  set_color: [["id"], ["color"]],
763
822
  set_width: [["id"], ["width"]],
764
823
  set_style: [["id"], ["style"]],
765
824
  set_extend: [["id"], ["extend"]],
825
+ set_xloc: [["id"], ["xloc"]],
766
826
  delete: [["id"]],
767
827
  get_price: [["id"], ["x"]],
768
828
  get_x1: [["id"]],
@@ -773,44 +833,144 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
773
833
  box: {
774
834
  set_corners: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
775
835
  set_lefttop: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
836
+ set_left: [["id"], ["left"]],
837
+ set_right: [["id"], ["right"]],
838
+ set_top: [["id"], ["top"]],
839
+ set_bottom: [["id"], ["bottom"]],
776
840
  set_bgcolor: [["id"], ["bgcolor", "color"]],
777
841
  set_border_color: [["id"], ["color", "border_color"]],
842
+ set_border_width: [["id"], ["width"]],
843
+ set_border_style: [["id"], ["style"]],
778
844
  set_text: [["id"], ["text"]],
845
+ set_text_color: [["id"], ["color", "text_color"]],
846
+ set_text_font_family: [["id"], ["font_family"]],
847
+ set_text_halign: [["id"], ["halign"]],
848
+ set_text_valign: [["id"], ["valign"]],
849
+ set_text_size: [["id"], ["size"]],
850
+ set_text_formatting: [["id"], ["formatting"]],
851
+ set_text_wrap: [["id"], ["wrap"]],
779
852
  set_extend: [["id"], ["extend"]],
853
+ set_closed: [["id"], ["closed"]],
854
+ set_rightbottom: [["id"], ["right"], ["bottom"]],
855
+ get_left: [["id"]],
856
+ get_right: [["id"]],
857
+ get_top: [["id"]],
858
+ get_bottom: [["id"]],
780
859
  delete: [["id"]],
781
860
  },
782
861
  table: {
783
862
  cell: [["id"], ["column", "col"], ["row"], ["text"]],
784
- cell_set_text: [["id"], ["column", "col"], ["row"], ["text"]],
863
+ // Interpret positional order is (id, row, column, text), not column-first.
864
+ cell_set_text: [["id"], ["row"], ["column", "col"], ["text"]],
865
+ cell_get_text: [["id"], ["row"], ["column", "col"]],
866
+ cell_set_text_color: [["id"], ["row"], ["column", "col"], ["color", "text"]],
867
+ cell_set_bgcolor: [["id"], ["row"], ["column", "col"], ["color"]],
868
+ cell_set_border_color: [["id"], ["row"], ["column", "col"], ["color"]],
869
+ cell_set_border_width: [["id"], ["row"], ["column", "col"], ["width"]],
870
+ cell_set_tooltip: [["id"], ["row"], ["column", "col"], ["tooltip", "text"]],
871
+ cell_set_width: [["id"], ["column", "col"], ["row"], ["width"]],
872
+ cell_set_height: [["id"], ["column", "col"], ["row"], ["height"]],
873
+ cell_set_text_halign: [["id"], ["column", "col"], ["row"], ["halign"]],
874
+ cell_set_text_valign: [["id"], ["column", "col"], ["row"], ["valign"]],
875
+ cell_set_text_size: [["id"], ["column", "col"], ["row"], ["size"]],
876
+ cell_set_text_font_family: [["id"], ["column", "col"], ["row"], ["font_family"]],
877
+ cell_set_text_formatting: [["id"], ["column", "col"], ["row"], ["formatting"]],
785
878
  set_position: [["id"], ["position"]],
879
+ set_bgcolor: [["id"], ["color", "bgcolor"]],
880
+ set_border_color: [["id"], ["color"]],
881
+ set_border_width: [["id"], ["width"]],
882
+ set_frame_color: [["id"], ["color"]],
883
+ set_frame_width: [["id"], ["width"]],
884
+ merge_cells: [["id"], ["start_row"], ["start_column", "start_col"], ["end_row"], ["end_column", "end_col"]],
786
885
  delete: [["id"]],
787
886
  },
788
887
  polyline: {
789
888
  delete: [["id"]],
889
+ get_points: [["id"]],
890
+ set_points: [["id"], ["points"]],
891
+ set_line_color: [["id"], ["color"]],
892
+ set_line_width: [["id"], ["width"]],
893
+ set_line_style: [["id"], ["style"]],
894
+ set_fill_color: [["id"], ["fill_color", "color"]],
895
+ set_curved: [["id"], ["curved"]],
896
+ set_force_overlay: [["id"], ["force_overlay"]],
897
+ set_closed: [["id"], ["closed"]],
898
+ set_xloc: [["id"], ["xloc"]],
790
899
  },
791
900
  linefill: {
792
901
  set_color: [["id"], ["color"]],
793
902
  delete: [["id"]],
903
+ get_line1: [["id"]],
904
+ get_line2: [["id"]],
794
905
  },
795
906
  };
796
907
 
908
+ function emitChartPoint(name: string, args: CallArgs): string | null {
909
+ const meth = name.slice("chart_point_".length);
910
+ if (meth === "new" || meth === "from_time") {
911
+ return `__h.draw.point_new(${pick(args, 0, ["time"], "null")}, ${pick(args, 1, ["price"], "0")})`;
912
+ }
913
+ if (meth === "from_index") {
914
+ return `__h.draw.point_from_index(${pick(args, 0, ["index"], "null")}, ${pick(args, 1, ["price"], "0")})`;
915
+ }
916
+ if (meth === "now") {
917
+ return `__h.draw.point_now(__bar_idx, time, ${pick(args, 0, ["price"], "0")})`;
918
+ }
919
+ if (meth === "copy") {
920
+ return `__h.draw.point_copy(${pick(args, 0, ["point"], "null")})`;
921
+ }
922
+ return null;
923
+ }
924
+
797
925
  function emitDraw(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
798
926
  if (name === "alert" && methodSrc == null) {
799
927
  ctx.usesDrawings = true;
800
928
  return `__h.draw.alert(__bar_idx, ${pick(args, 0, ["message"], "null")})`;
801
929
  }
930
+ if (name.startsWith("chart_point_")) {
931
+ const point = emitChartPoint(name, args);
932
+ if (point != null) ctx.usesDrawings = true;
933
+ return point;
934
+ }
802
935
  const parsed = drawKindAndMethod(name, methodSrc);
803
936
  if (parsed == null) return null;
804
937
  const { kind, method } = parsed;
805
938
  let code: string | null = null;
806
- if (method === "new") {
939
+ if (method === "all") {
940
+ code = `__h.draw.all(${JSON.stringify(kind)})`;
941
+ } else if (method === "copy" && (kind === "line" || kind === "label" || kind === "box" || kind === "polyline")) {
942
+ code = `__h.draw.copy(${pick(args, 0, ["id"], "null")})`;
943
+ } else if (method === "new") {
807
944
  if (kind === "label") {
808
- code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")})`;
945
+ code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")}, ${pick(args, 0, ["x"], "0")}, ${pick(args, 1, ["y"], "0")})`;
946
+ } else if (kind === "line") {
947
+ code = `__h.draw.line_new(__bar_idx, ${pick(args, 0, ["x1"], "0")}, ${pick(args, 1, ["y1"], "0")}, ${pick(args, 2, ["x2"], "0")}, ${pick(args, 3, ["y2"], "0")})`;
948
+ } else if (kind === "box") {
949
+ code = `__h.draw.box_new(__bar_idx, ${pick(args, 0, ["left"], "0")}, ${pick(args, 1, ["top"], "0")}, ${pick(args, 2, ["right"], "0")}, ${pick(args, 3, ["bottom"], "0")})`;
950
+ } else if (kind === "polyline") {
951
+ code = `__h.draw.polyline_new(__bar_idx, ${pick(args, 0, ["points"], "null")})`;
809
952
  } else if (kind === "linefill") {
810
- code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1"], "null")}, ${pick(args, 1, ["id2"], "null")})`;
953
+ const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
954
+ code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
811
955
  } else if (DRAW_KINDS.has(kind)) {
812
956
  code = `__h.draw.${kind}_new(__bar_idx)`;
813
957
  }
958
+ } else if (kind === "line" && method === "fill") {
959
+ const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
960
+ code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
961
+ } else if (
962
+ (kind === "line" && (method === "set_first_point" || method === "set_second_point")) ||
963
+ (kind === "label" && method === "set_point") ||
964
+ (kind === "box" && (method === "set_top_left_point" || method === "set_bottom_right_point"))
965
+ ) {
966
+ code = `__h.draw.${kind}_${method}(${pick(args, 0, ["id"], "null")}, ${pick(args, 1, ["point"], "null")})`;
967
+ } else if (kind === "box" && method === "set_xloc") {
968
+ code = `__h.draw.box_set_xloc(${pick(args, 0, ["id"], "null")}, ${pick(args, 1, ["left"], "null")}, ${pick(args, 2, ["right"], "null")}, ${pick(args, 3, ["xloc"], "null")})`;
969
+ } else if (kind === "table" && method === "clear") {
970
+ const id = pick(args, 0, ["id", "table_id"], "null");
971
+ code = hasArg(args, 1, ["start_row"])
972
+ ? `__h.draw.table_clear(${id}, ${pick(args, 1, ["start_row"], "null")}, ${pick(args, 2, ["start_column", "start_col"], "null")}, ${pick(args, 3, ["end_row"], "null")}, ${pick(args, 4, ["end_column", "end_col"], "null")})`
973
+ : `__h.draw.table_clear(${id})`;
814
974
  } else {
815
975
  const aliased = kind === "box" && method === "set_lefttop" ? "set_corners" : method;
816
976
  const params = DRAW_METHODS[kind]?.[method] ?? DRAW_METHODS[kind]?.[aliased];
@@ -855,6 +1015,7 @@ function emitUdt(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallA
855
1015
 
856
1016
  const SECURITY_FUNCS = new Set([
857
1017
  "security",
1018
+ "security_lower_tf",
858
1019
  "request_security",
859
1020
  "request_security_lower_tf",
860
1021
  "request_seed",
@@ -1024,6 +1185,7 @@ function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
1024
1185
  add(["default_qty_type"], "default_qty_type");
1025
1186
  add(["default_qty_value"], "default_qty_value");
1026
1187
  add(["initial_capital"], "initial_capital");
1188
+ add(["currency"], "currency");
1027
1189
  return `__h.strategy.configure({${fields.join(", ")}})`;
1028
1190
  }
1029
1191
 
@@ -1095,8 +1257,50 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
1095
1257
  case "margin_liquidation_price":
1096
1258
  case "position_avg_price":
1097
1259
  case "initial_capital":
1260
+ case "cash":
1261
+ case "position_entry_name":
1262
+ case "account_currency":
1263
+ case "max_contracts_held_all":
1264
+ case "max_contracts_held_long":
1265
+ case "max_contracts_held_short":
1266
+ case "wintrades":
1267
+ case "losstrades":
1268
+ case "eventrades":
1269
+ case "grossprofit":
1270
+ case "grossloss":
1271
+ case "avg_trade":
1272
+ case "avg_winning_trade":
1273
+ case "avg_losing_trade":
1274
+ case "netprofit_percent":
1275
+ case "openprofit_percent":
1276
+ case "grossprofit_percent":
1277
+ case "grossloss_percent":
1278
+ case "avg_trade_percent":
1279
+ case "avg_winning_trade_percent":
1280
+ case "avg_losing_trade_percent":
1281
+ case "max_drawdown":
1282
+ case "max_drawdown_percent":
1283
+ case "max_runup":
1284
+ case "max_runup_percent":
1285
+ case "percent_profitable":
1286
+ case "winrate":
1287
+ case "profitfactor":
1288
+ case "profit_factor":
1289
+ case "capital_held":
1290
+ case "closedtrades_first_index":
1098
1291
  ctx.usesStrategy = true;
1099
1292
  return `__h.strategy.${method}()`;
1293
+ case "commission":
1294
+ return "0";
1295
+ case "default_entry_qty":
1296
+ ctx.usesStrategy = true;
1297
+ return hasArg(args, 0, ["percent_equity"])
1298
+ ? `__h.strategy.default_entry_qty(${pick(args, 0, ["percent_equity"], "null")})`
1299
+ : `__h.strategy.default_entry_qty()`;
1300
+ case "convert_to_account":
1301
+ case "convert_to_symbol":
1302
+ ctx.usesStrategy = true;
1303
+ return hasArg(args, 0, ["value"]) ? `__h.strategy.convert_to(${pick(args, 0, ["value"], "null")})` : "1";
1100
1304
  case "risk_allow_entry_in":
1101
1305
  ctx.usesStrategy = true;
1102
1306
  return `__h.strategy.risk_allow_entry_in(${pick(args, 0, ["value"], "null")})`;
@@ -1106,9 +1310,33 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
1106
1310
  case "risk_max_drawdown":
1107
1311
  ctx.usesStrategy = true;
1108
1312
  return `__h.strategy.risk_max_drawdown(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["type"], "null")})`;
1109
- default:
1313
+ default: {
1314
+ if (method.startsWith("direction_")) {
1315
+ const tail = method.slice("direction_".length);
1316
+ if (tail === "long" || tail === "short" || tail === "all") return JSON.stringify(tail);
1317
+ }
1318
+ if (method.startsWith("oca_")) {
1319
+ const tail = method.slice("oca_".length);
1320
+ if (tail === "none" || tail === "cancel" || tail === "reduce") return JSON.stringify(tail);
1321
+ }
1322
+ if (method.startsWith("commission_")) {
1323
+ const tail = method.slice("commission_".length);
1324
+ if (tail === "percent" || tail === "cash_per_order" || tail === "cash_per_contract") {
1325
+ return JSON.stringify(tail);
1326
+ }
1327
+ }
1328
+ if (method.startsWith("closedtrades_") || method.startsWith("opentrades_")) {
1329
+ ctx.usesStrategy = true;
1330
+ if (method === "opentrades_capital_held") return "__h.strategy.capital_held()";
1331
+ if (method === "closedtrades_first_index") return "__h.strategy.closedtrades_first_index()";
1332
+ const idx = hasArg(args, 0, ["trade_num", "trade_index", "index"])
1333
+ ? pick(args, 0, ["trade_num", "trade_index", "index"], "0")
1334
+ : "0";
1335
+ return `__h.strategy.${method}(${idx})`;
1336
+ }
1110
1337
  if (isStrategyAction(name, method)) ctx.usesStrategy = true;
1111
1338
  return "null";
1339
+ }
1112
1340
  }
1113
1341
  }
1114
1342
 
@@ -1167,7 +1395,7 @@ function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
1167
1395
  : pick(args, 0, ["series", "source", "price"], "null");
1168
1396
  const i = ctx.plots.length;
1169
1397
  ctx.plots.push({ title });
1170
- return `plot_${i}[__bar_idx] = __h.naNum(${series})`;
1398
+ return `plot_${i}[__bar_idx] = __h.plotNum(${series})`;
1171
1399
  }
1172
1400
 
1173
1401
  function emitColor(name: string, args: CallArgs): string | null {
@@ -1792,5 +2020,80 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1792
2020
  ]);
1793
2021
  return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
1794
2022
  }
2023
+ if (method === "hammer") {
2024
+ return `__h.ta.hammer(${cellOf(pick(args, 0, ["open"], "null"))}, ${cellOf(pick(args, 1, ["high"], "null"))}, ${cellOf(pick(args, 2, ["low"], "null"))}, ${cellOf(pick(args, 3, ["close"], "null"))})`;
2025
+ }
2026
+ if (method === "gap_detector") {
2027
+ return `__h.ta.gapDetector(${cellOf(pick(args, 0, ["high"], "null"))}, ${cellOf(pick(args, 1, ["low"], "null"))}, ${cellOf(pick(args, 2, ["prev_close", "prev"], "null"))})`;
2028
+ }
2029
+ if (method === "macd_signal") {
2030
+ return `__h.ta.macdSignal(${cellOf(pick(args, 0, ["macd_line", "macd"], "null"))}, ${cellOf(pick(args, 1, ["signal_line", "signal"], "null"))})`;
2031
+ }
2032
+ if (method === "acceleration_factor") {
2033
+ return taCall("accelerationFactor", site, [
2034
+ cellOf(pick(args, 0, ["momentum_list", "momentum", "source", "series"], "null")),
2035
+ cellOf(pick(args, 1, ["period", "length"], "null")),
2036
+ ]);
2037
+ }
2038
+ if (method === "stoch_smooth") {
2039
+ return `__h.ta.stochSmooth(${cellOf(pick(args, 0, ["high"], "null"))}, ${cellOf(pick(args, 1, ["low"], "null"))}, ${cellOf(pick(args, 2, ["close"], "null"))}, ${cellOf(pick(args, 3, ["period", "length"], "null"))}, ${cellOf(pick(args, 4, ["smooth_k", "k"], "null"))}, ${cellOf(pick(args, 5, ["smooth_d", "d"], "null"))})`;
2040
+ }
2041
+ if (method === "engulfing") {
2042
+ // Bar-mode sentinel. Args are still evaluated (interpret reads all four).
2043
+ return `(() => { ${cellOf(pick(args, 0, ["open"], "null"))}; ${cellOf(pick(args, 1, ["high"], "null"))}; ${cellOf(pick(args, 2, ["low"], "null"))}; ${cellOf(pick(args, 3, ["close"], "null"))}; return __h.ta.engulfing(); })()`;
2044
+ }
2045
+ if (method === "inside_bar_pattern") {
2046
+ return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.insideBarPattern(); })()`;
2047
+ }
2048
+ if (method === "double_top_bottom") {
2049
+ return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.doubleTopBottom(${cellOf(pick(args, 2, ["period", "length"], "null"))}); })()`;
2050
+ }
2051
+ if (method === "voi") {
2052
+ return `__h.ta.voi(${cellOf(pick(args, 0, ["buy_volume", "buy"], "null"))}, ${cellOf(pick(args, 1, ["sell_volume", "sell"], "null"))})`;
2053
+ }
2054
+ if (method === "bid_ask_imbalance") {
2055
+ return `__h.ta.bidAskImbalance(${cellOf(pick(args, 0, ["bid_size", "bid"], "null"))}, ${cellOf(pick(args, 1, ["ask_size", "ask"], "null"))}, ${cellOf(pick(args, 2, ["bid_price"], "null"))}, ${cellOf(pick(args, 3, ["ask_price"], "null"))})`;
2056
+ }
2057
+ if (method === "rsi_oversold_overbought") {
2058
+ return `__h.ta.rsiOversoldOverbought(${cellOf(pick(args, 0, ["rsi_series", "rsi", "source"], "null"))}, ${cellOf(pick(args, 1, ["oversold_level", "oversold"], "null"))}, ${cellOf(pick(args, 2, ["overbought_level", "overbought"], "null"))})`;
2059
+ }
2060
+ if (method === "rsi_divergence") {
2061
+ // Missing period is 0 (interpret `period ?? 0`), not the engine's null → na.
2062
+ const period = `((__n) => (__n == null ? 0 : __n))(${cellOf(pick(args, 1, ["period", "length"], "null"))})`;
2063
+ return `__h.ta.rsiDivergence(${cellList(pick(args, 0, ["rsi_series", "series", "source"], "null"))}, ${period})`;
2064
+ }
2065
+ if (method === "cumulative_delta") {
2066
+ return taCall("cumulativeDelta", site, [
2067
+ cellOf(pick(args, 0, ["close", "source"], "null")),
2068
+ cellOf(pick(args, 1, ["volume"], "null")),
2069
+ cellOf(pick(args, 2, ["period", "length"], "null")),
2070
+ ]);
2071
+ }
2072
+ if (method === "volume_momentum") {
2073
+ return taCall("volumeMomentum", site, [
2074
+ cellOf(pick(args, 0, ["volume", "source"], "null")),
2075
+ cellOf(pick(args, 1, ["period", "length"], "null")),
2076
+ ]);
2077
+ }
2078
+ if (method === "volume_profile_high") {
2079
+ return `__h.ta.volumeProfileHigh(${cellList(pick(args, 0, ["close", "price", "source", "series"], "null"))}, ${cellList(pick(args, 1, ["volume"], "null"))})`;
2080
+ }
2081
+ if (method === "volume_profile_low") {
2082
+ return taCall("volumeProfileLow", site, [
2083
+ cellOf(pick(args, 0, ["close", "source"], "null")),
2084
+ cellOf(pick(args, 1, ["volume"], "null")),
2085
+ cellOf(pick(args, 2, ["period", "length"], "null")),
2086
+ cellOf(pick(args, 3, ["levels"], "null")),
2087
+ ]);
2088
+ }
2089
+ if (method === "volume_thrust") {
2090
+ const sens = cellOf(pick(args, 3, ["sensitivity"], "null"));
2091
+ return taCall("volumeThrust", site, [
2092
+ cellOf(pick(args, 0, ["close", "source"], "null")),
2093
+ cellOf(pick(args, 1, ["volume"], "null")),
2094
+ cellOf(pick(args, 2, ["volume_sma", "vol_sma", "sma"], "null")),
2095
+ `((__n) => (__n == null ? 0.3 : __n))(${sens})`,
2096
+ ]);
2097
+ }
1795
2098
  return "null";
1796
2099
  }
@@ -83,6 +83,7 @@ import {
83
83
  safeNeg,
84
84
  safeSub,
85
85
  } from "./helpers.ts";
86
+ import { isStrategyCashAmount } from "../strategy.ts";
86
87
  import { createCompileStrategy } from "./strategy.ts";
87
88
  import { udtGet, udtNew, udtRegister, udtSet } from "./udt_runtime.ts";
88
89
 
@@ -105,6 +106,8 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
105
106
  na: NA_FN,
106
107
  nz,
107
108
  naNum,
109
+ plotNum,
110
+ timenow,
108
111
  isNa: isNaCell,
109
112
  eq: pineEq,
110
113
  ne: pineNe,
@@ -264,6 +267,23 @@ function iff(cond: unknown, thenV: unknown, elseV: unknown): unknown {
264
267
  return mathIff(cond as number | null, thenV as number | null, elseV as number | null);
265
268
  }
266
269
 
270
+ /** Plot cell: tagged `strategy.cash` unwraps to its number (interpret `unwrap`). */
271
+ function plotNum(v: unknown): number | null {
272
+ if (isStrategyCashAmount(v)) return Number.isFinite(v.value) ? v.value : null;
273
+ return naNum(v);
274
+ }
275
+
276
+ /**
277
+ * Dataset last-bar time on every bar. Interpret `beginBar` stores that on
278
+ * `ctx.timenow` (host seed is overwritten). Not wall-clock, not current bar.
279
+ */
280
+ function timenow(timeArr: unknown): number {
281
+ if (!Array.isArray(timeArr) || timeArr.length === 0) return 0;
282
+ const last = timeArr[timeArr.length - 1];
283
+ if (typeof last === "number" && Number.isFinite(last)) return last;
284
+ return (timeArr.length - 1) * 60_000;
285
+ }
286
+
267
287
  function NA_FN(v?: unknown): number | null {
268
288
  if (arguments.length === 0) return null;
269
289
  return isNaCell(v) ? 1 : 0;