@hoox-sh/pynets 0.2.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.
@@ -13,6 +13,18 @@ const LOW = "low_arr[__bar_idx]";
13
13
  const CLOSE = "close_arr[__bar_idx]";
14
14
  const VOL = "vol_arr[__bar_idx]";
15
15
  const OPEN = "open_arr[__bar_idx]";
16
+ // Interpret's bare `ta.vwap` sources hlc3 = (h+l+c)/3, falling back to close
17
+ // when any component is na — mirror that exactly on the compile side.
18
+ const HLC3 =
19
+ "((high_arr[__bar_idx] != null && low_arr[__bar_idx] != null && close_arr[__bar_idx] != null)" +
20
+ " ? (high_arr[__bar_idx] + low_arr[__bar_idx] + close_arr[__bar_idx]) / 3" +
21
+ " : close_arr[__bar_idx])";
22
+ // Interpret `hlc3(env)` for the ta.cci period-only form: falls back through
23
+ // close → high → low when any component is na (interpret.ts hlc3).
24
+ const TP =
25
+ "((high_arr[__bar_idx] != null && low_arr[__bar_idx] != null && close_arr[__bar_idx] != null)" +
26
+ " ? (high_arr[__bar_idx] + low_arr[__bar_idx] + close_arr[__bar_idx]) / 3" +
27
+ " : (close_arr[__bar_idx] ?? high_arr[__bar_idx] ?? low_arr[__bar_idx]))";
16
28
 
17
29
  const NS = new Set([
18
30
  "ta",
@@ -87,13 +99,19 @@ const TA_METHODS = new Set([
87
99
  "cum",
88
100
  "range",
89
101
  "accdist",
102
+ "ad",
90
103
  "pvt",
104
+ "vpt",
91
105
  "wad",
92
106
  "nvi",
93
107
  "pvi",
94
108
  "wvad",
109
+ "cmf",
110
+ "klinger",
95
111
  "iii",
96
112
  "obv",
113
+ "ao",
114
+ "aroon",
97
115
  "highestbars",
98
116
  "lowestbars",
99
117
  "dev",
@@ -106,6 +124,57 @@ const TA_METHODS = new Set([
106
124
  "bbw",
107
125
  "max",
108
126
  "min",
127
+ "uo",
128
+ "rci",
129
+ "dpo",
130
+ "kst",
131
+ "stochrsi",
132
+ "donchian",
133
+ "ichimoku",
134
+ "bb_pct",
135
+ "emv",
136
+ "fractal",
137
+ "atr_stop",
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",
156
+ ]);
157
+
158
+ /**
159
+ * Bare `ta.<attr>` auto-call whitelist. Mirrors interpret's evalTaAttr
160
+ * (interpret.ts) exactly: only these attributes auto-invoke a zero-arg TA
161
+ * kernel on a bare attribute load; anything else is na on both backends.
162
+ * `aroon` is deliberately absent — interpret's evalTaAttr does not handle it.
163
+ */
164
+ export const TA_BARE_ATTRS = new Set([
165
+ "accdist",
166
+ "ad",
167
+ "obv",
168
+ "tr",
169
+ "vwap",
170
+ "pvt",
171
+ "vpt",
172
+ "nvi",
173
+ "pvi",
174
+ "wad",
175
+ "wvad",
176
+ "cmf",
177
+ "ao",
109
178
  ]);
110
179
 
111
180
  const SRC_LEN = new Set([
@@ -120,8 +189,6 @@ const SRC_LEN = new Set([
120
189
  "sum",
121
190
  "dema",
122
191
  "tema",
123
- "highestbars",
124
- "lowestbars",
125
192
  "dev",
126
193
  "variance",
127
194
  "median",
@@ -141,6 +208,8 @@ const PLOT_FUNCS = new Set([
141
208
  "hline",
142
209
  "bgcolor",
143
210
  "barcolor",
211
+ "plotcandle",
212
+ "plotbar",
144
213
  ]);
145
214
 
146
215
  const STRATEGY_ACTIONS = new Set([
@@ -160,6 +229,10 @@ const STRATEGY_QUERIES = new Set([
160
229
  "strategy_openprofit",
161
230
  "strategy_opentrades",
162
231
  "strategy_closedtrades",
232
+ "strategy_leverage",
233
+ "strategy_margin_liquidation_price",
234
+ "strategy_position_avg_price",
235
+ "strategy_initial_capital",
163
236
  ]);
164
237
 
165
238
  const CHART_SERIES_ARR: Record<string, string> = {
@@ -232,7 +305,7 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
232
305
  if (PLOT_FUNCS.has(name)) return emitPlot(ctx, name, args);
233
306
 
234
307
  if (name === "indicator" || name === "study" || name === "library") return "null";
235
- if (name === "strategy") return "null";
308
+ if (name === "strategy") return emitStrategyDecl(ctx, args);
236
309
 
237
310
  if (name === "na") {
238
311
  if (args.pos.length === 0 && args.kw.x == null && args.kw.source == null) return "null";
@@ -275,6 +348,19 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
275
348
  }
276
349
  return "86400";
277
350
  }
351
+ if (name === "timeframe_change") {
352
+ // Prev bar time is `time_arr[__bar_idx-1]`; bar 0 has no prev (interpret series.get(1)).
353
+ return `__h.timeframeChange(${pick(args, 0, ["timeframe", "tf"], "null")}, time, (__bar_idx > 0 ? time_arr[__bar_idx - 1] : null), __bar_idx)`;
354
+ }
355
+ if (name === "time_close") {
356
+ return `__h.timeClose(__bar_idx, time_arr)`;
357
+ }
358
+ if (name === "syminfo_prefix") {
359
+ if (hasArg(args, 0, ["tickerid", "symbol"])) {
360
+ return `__h.syminfoPrefix(${pick(args, 0, ["tickerid", "symbol"], "null")})`;
361
+ }
362
+ return `__h.syminfoPrefix()`;
363
+ }
278
364
  if (name === "ticker_heikinashi" || name === "heikinashi") {
279
365
  ctx.needsHeikinashi = true;
280
366
  return `"__HEIKINASHI__"`;
@@ -298,7 +384,15 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
298
384
  return `(() => { throw new Error(String(${msg})); })()`;
299
385
  }
300
386
 
301
- 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
+ }
302
396
  if (name === "str_tostring" || name === "tostring") {
303
397
  const x = pick(args, 0, ["value", "source", "x"], "null");
304
398
  return `String(${x} ?? "")`;
@@ -432,6 +526,43 @@ function hasArg(args: CallArgs, index: number, names: string[]): boolean {
432
526
  return index >= 0 && index < args.pos.length;
433
527
  }
434
528
 
529
+ /** True when a keyword argument named one of *names* is present (interpret hasKwarg). */
530
+ function hasAnyKw(args: CallArgs, names: string[]): boolean {
531
+ return names.some((n) => Object.hasOwn(args.kw, n));
532
+ }
533
+
534
+ /**
535
+ * Runtime-safe `_is_period_like` gate (interpret `periodLikeCell`): only whole
536
+ * numbers pass and are returned as-is; everything else (na, fractional,
537
+ * series samples) collapses to *fb* — mirroring `periodLikeArg(...) ?? fb`.
538
+ * Constant args could be decided at emit time, but the runtime form is
539
+ * equally exact and keeps one code path for constants and series alike.
540
+ */
541
+ function periodGate(expr: string, fb: string): string {
542
+ return `((__p) => (Number.isInteger(__p) ? __p : ${fb}))(${expr})`;
543
+ }
544
+
545
+ /**
546
+ * Interpret `lenArg` / `lenOrDefault` analog: a null (na) or non-finite slot
547
+ * collapses to *fb* instead of reaching the kernel raw.
548
+ */
549
+ function finiteOr(expr: string, fb: string): string {
550
+ return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
551
+ }
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
+
435
566
  const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
436
567
 
437
568
  const ARRAY_SIGS: Record<string, string[][]> = {
@@ -452,7 +583,7 @@ const ARRAY_SIGS: Record<string, string[][]> = {
452
583
  fill: [["id"], ["value"]],
453
584
  copy: [["id"]],
454
585
  reverse: [["id"]],
455
- sort: [["id"], ["order"]],
586
+ sort: [["id"], ["order"], ["sort_field"]],
456
587
  concat: [["id"], ["other", "id2"]],
457
588
  slice: [["id"], ["index_from", "from"], ["index_to", "to"]],
458
589
  indexof: [["id"], ["value"]],
@@ -476,10 +607,10 @@ const ARRAY_SIGS: Record<string, string[][]> = {
476
607
  percentile_linear_interpolation: [["id"], ["percentage", "percent"]],
477
608
  percentile_nearest: [["id"], ["percentage", "percent"]],
478
609
  percentile_nearest_rank: [["id"], ["percentage", "percent"]],
479
- binary_search: [["id"], ["value"]],
480
- binary_search_leftmost: [["id"], ["value"]],
481
- binary_search_rightmost: [["id"], ["value"]],
482
- sort_indices: [["id"], ["order"]],
610
+ binary_search: [["id"], ["value"], ["sort_field"]],
611
+ binary_search_leftmost: [["id"], ["value"], ["sort_field"]],
612
+ binary_search_rightmost: [["id"], ["value"], ["sort_field"]],
613
+ sort_indices: [["id"], ["order"], ["sort_field"]],
483
614
  covariance: [["id"], ["id2", "other"], ["biased"]],
484
615
  };
485
616
 
@@ -541,7 +672,8 @@ const MATRIX_SIGS: Record<string, string[][]> = {
541
672
  concat: [["id"], ["other"]],
542
673
  reshape: [["id"], ["rows"], ["columns", "cols"]],
543
674
  reverse: [["id"]],
544
- sort: [["id"], ["column", "col"], ["order"]],
675
+ sort: [["id"], ["column", "col"], ["order"], ["sort_field"]],
676
+ sort_indices: [["id"], ["column", "col"], ["order"], ["sort_field"]],
545
677
  eigenvalues: [["id"]],
546
678
  eigenvectors: [["id"]],
547
679
  pow: [["id"], ["power", "n"]],
@@ -554,6 +686,26 @@ const MATRIX_SIGS: Record<string, string[][]> = {
554
686
  remove_col: [["id"], ["column", "index"]],
555
687
  remove_column: [["id"], ["column", "index"]],
556
688
  submatrix: [["id"], ["from_row"], ["to_row"], ["from_column", "from_col"], ["to_column", "to_col"]],
689
+ sum_row: [["id"], ["row", "index"]],
690
+ avg_row: [["id"], ["row", "index"]],
691
+ min_row: [["id"], ["row", "index"]],
692
+ max_row: [["id"], ["row", "index"]],
693
+ mode_row: [["id"], ["row", "index"]],
694
+ sum_col: [["id"], ["column", "col", "index"]],
695
+ avg_col: [["id"], ["column", "col", "index"]],
696
+ min_col: [["id"], ["column", "col", "index"]],
697
+ max_col: [["id"], ["column", "col", "index"]],
698
+ mode_col: [["id"], ["column", "col", "index"]],
699
+ copy_row: [["id"], ["row", "index"]],
700
+ copy_col: [["id"], ["column", "col", "index"]],
701
+ fill_row: [["id"], ["row", "index"], ["value"]],
702
+ fill_col: [["id"], ["column", "col", "index"], ["value"]],
703
+ fill_diagonal: [["id"], ["value"]],
704
+ reverse_rows: [["id"]],
705
+ reverse_cols: [["id"]],
706
+ stdev: [["id"]],
707
+ variance: [["id"]],
708
+ mode_all: [["id"]],
557
709
  };
558
710
 
559
711
  function nsMethod(name: string, prefix: string): string | null {
@@ -562,6 +714,8 @@ function nsMethod(name: string, prefix: string): string | null {
562
714
  }
563
715
 
564
716
  function ctorMethod(method: string): string {
717
+ // Every Python `array.new*` spelling (newint, new_float, new_chart.point, …)
718
+ // lowers to the same PineArray constructor as `array.new`.
565
719
  return method === "from" || method.startsWith("new") ? "new" : method;
566
720
  }
567
721
 
@@ -588,6 +742,10 @@ function emitHostCall(ns: string, method: string, args: CallArgs, params: string
588
742
  function emitArray(name: string, args: CallArgs): string | null {
589
743
  const raw = nsMethod(name, "array_");
590
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
+ }
591
749
  const method = ctorMethod(raw);
592
750
  const params = ARRAY_SIGS[method];
593
751
  if (params == null) return null;
@@ -629,11 +787,24 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
629
787
  label: {
630
788
  set_text: [["id"], ["text"]],
631
789
  set_xy: [["id"], ["x"], ["y"]],
790
+ set_x: [["id"], ["x"]],
791
+ set_y: [["id"], ["y"]],
632
792
  set_color: [["id"], ["color"]],
633
793
  set_style: [["id"], ["style"]],
634
794
  set_size: [["id"], ["size"]],
635
795
  set_tooltip: [["id"], ["tooltip"]],
636
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"]],
637
808
  delete: [["id"]],
638
809
  get_text: [["id"]],
639
810
  get_x: [["id"]],
@@ -641,12 +812,17 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
641
812
  },
642
813
  line: {
643
814
  set_xy: [["id"], ["x1"], ["y1"], ["x2"], ["y2"]],
644
- set_xy1: [["id"], ["x1"], ["y1"]],
645
- 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"]],
646
821
  set_color: [["id"], ["color"]],
647
822
  set_width: [["id"], ["width"]],
648
823
  set_style: [["id"], ["style"]],
649
824
  set_extend: [["id"], ["extend"]],
825
+ set_xloc: [["id"], ["xloc"]],
650
826
  delete: [["id"]],
651
827
  get_price: [["id"], ["x"]],
652
828
  get_x1: [["id"]],
@@ -657,44 +833,144 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
657
833
  box: {
658
834
  set_corners: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
659
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"]],
660
840
  set_bgcolor: [["id"], ["bgcolor", "color"]],
661
841
  set_border_color: [["id"], ["color", "border_color"]],
842
+ set_border_width: [["id"], ["width"]],
843
+ set_border_style: [["id"], ["style"]],
662
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"]],
663
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"]],
664
859
  delete: [["id"]],
665
860
  },
666
861
  table: {
667
862
  cell: [["id"], ["column", "col"], ["row"], ["text"]],
668
- 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"]],
669
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"]],
670
885
  delete: [["id"]],
671
886
  },
672
887
  polyline: {
673
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"]],
674
899
  },
675
900
  linefill: {
676
901
  set_color: [["id"], ["color"]],
677
902
  delete: [["id"]],
903
+ get_line1: [["id"]],
904
+ get_line2: [["id"]],
678
905
  },
679
906
  };
680
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
+
681
925
  function emitDraw(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
682
926
  if (name === "alert" && methodSrc == null) {
683
927
  ctx.usesDrawings = true;
684
928
  return `__h.draw.alert(__bar_idx, ${pick(args, 0, ["message"], "null")})`;
685
929
  }
930
+ if (name.startsWith("chart_point_")) {
931
+ const point = emitChartPoint(name, args);
932
+ if (point != null) ctx.usesDrawings = true;
933
+ return point;
934
+ }
686
935
  const parsed = drawKindAndMethod(name, methodSrc);
687
936
  if (parsed == null) return null;
688
937
  const { kind, method } = parsed;
689
938
  let code: string | null = null;
690
- 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") {
691
944
  if (kind === "label") {
692
- 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")})`;
693
952
  } else if (kind === "linefill") {
694
- 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})`;
695
955
  } else if (DRAW_KINDS.has(kind)) {
696
956
  code = `__h.draw.${kind}_new(__bar_idx)`;
697
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})`;
698
974
  } else {
699
975
  const aliased = kind === "box" && method === "set_lefttop" ? "set_corners" : method;
700
976
  const params = DRAW_METHODS[kind]?.[method] ?? DRAW_METHODS[kind]?.[aliased];
@@ -739,6 +1015,7 @@ function emitUdt(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallA
739
1015
 
740
1016
  const SECURITY_FUNCS = new Set([
741
1017
  "security",
1018
+ "security_lower_tf",
742
1019
  "request_security",
743
1020
  "request_security_lower_tf",
744
1021
  "request_seed",
@@ -891,6 +1168,27 @@ function isStrategyAction(name: string, method: string): boolean {
891
1168
  );
892
1169
  }
893
1170
 
1171
+ /** `strategy(...)` declaration — kwargs only, matching interpret `applyStrategyDecl`. */
1172
+ function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
1173
+ ctx.usesStrategy = true;
1174
+ const fields: string[] = [];
1175
+ const add = (names: string[], key: string): void => {
1176
+ if (hasArg(args, -1, names)) fields.push(`${key}: ${pick(args, -1, names, "null")}`);
1177
+ };
1178
+ add(["commission", "commission_value"], "commission");
1179
+ add(["slippage"], "slippage");
1180
+ add(["pyramiding"], "pyramiding");
1181
+ add(["avg_price_model"], "avg_price_model");
1182
+ add(["leverage"], "leverage");
1183
+ add(["margin_long"], "margin_long");
1184
+ add(["margin_short"], "margin_short");
1185
+ add(["default_qty_type"], "default_qty_type");
1186
+ add(["default_qty_value"], "default_qty_value");
1187
+ add(["initial_capital"], "initial_capital");
1188
+ add(["currency"], "currency");
1189
+ return `__h.strategy.configure({${fields.join(", ")}})`;
1190
+ }
1191
+
894
1192
  function emitStrategyEntry(method: "entry" | "order", args: CallArgs): string {
895
1193
  const id = pick(args, 0, ["id"], "null");
896
1194
  const direction = pick(args, 1, ["direction"], "null");
@@ -926,10 +1224,22 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
926
1224
  return "__h.strategy.close_all()";
927
1225
  case "exit": {
928
1226
  ctx.usesStrategy = true;
929
- const fromEntry = pick(args, 1, ["from_entry"], "null");
930
- return hasArg(args, 2, ["qty"])
931
- ? `__h.strategy.exit(${fromEntry}, ${pick(args, 2, ["qty"], "null")})`
932
- : `__h.strategy.exit(${fromEntry})`;
1227
+ const id = pick(args, 0, ["id"], "null");
1228
+ const fields: string[] = [];
1229
+ const add = (index: number, names: string[], key: string): void => {
1230
+ if (hasArg(args, index, names)) fields.push(`${key}: ${pick(args, index, names, "null")}`);
1231
+ };
1232
+ add(1, ["from_entry"], "from_entry");
1233
+ add(2, ["qty"], "qty");
1234
+ add(3, ["qty_percent"], "qty_percent");
1235
+ add(4, ["profit"], "profit");
1236
+ add(5, ["limit"], "limit");
1237
+ add(6, ["loss"], "loss");
1238
+ add(7, ["stop"], "stop");
1239
+ add(8, ["trail_price"], "trail_price");
1240
+ add(9, ["trail_points"], "trail_points");
1241
+ add(10, ["trail_offset"], "trail_offset");
1242
+ return `__h.strategy.exit(${id}, {${fields.join(", ")}})`;
933
1243
  }
934
1244
  case "cancel":
935
1245
  ctx.usesStrategy = true;
@@ -943,8 +1253,54 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
943
1253
  case "openprofit":
944
1254
  case "opentrades":
945
1255
  case "closedtrades":
1256
+ case "leverage":
1257
+ case "margin_liquidation_price":
1258
+ case "position_avg_price":
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":
946
1291
  ctx.usesStrategy = true;
947
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";
948
1304
  case "risk_allow_entry_in":
949
1305
  ctx.usesStrategy = true;
950
1306
  return `__h.strategy.risk_allow_entry_in(${pick(args, 0, ["value"], "null")})`;
@@ -954,9 +1310,33 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
954
1310
  case "risk_max_drawdown":
955
1311
  ctx.usesStrategy = true;
956
1312
  return `__h.strategy.risk_max_drawdown(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["type"], "null")})`;
957
- 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
+ }
958
1337
  if (isStrategyAction(name, method)) ctx.usesStrategy = true;
959
1338
  return "null";
1339
+ }
960
1340
  }
961
1341
  }
962
1342
 
@@ -992,7 +1372,7 @@ function packUserFuncArgs(ctx: EmitCtx, name: string, args: CallArgs): string[]
992
1372
  return packed;
993
1373
  }
994
1374
 
995
- function emitPlot(ctx: EmitCtx, _fname: string, args: CallArgs): string {
1375
+ function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
996
1376
  let title: string | null = constString(args.kwNodes.title);
997
1377
  if (title == null) {
998
1378
  for (const n of args.posNodes) {
@@ -1003,11 +1383,19 @@ function emitPlot(ctx: EmitCtx, _fname: string, args: CallArgs): string {
1003
1383
  }
1004
1384
  }
1005
1385
  }
1006
- if (title == null || title === "") title = `plot_${ctx.plots.length}`;
1007
- const series = pick(args, 0, ["series", "source", "price"], "null");
1386
+ if (title == null || title === "") {
1387
+ if (fname === "plotcandle") title = "candles";
1388
+ else if (fname === "plotbar") title = "bars";
1389
+ else title = `plot_${ctx.plots.length}`;
1390
+ }
1391
+ // Python primary column for plotcandle/plotbar is close (arg 3), not open.
1392
+ const ohlc = fname === "plotcandle" || fname === "plotbar";
1393
+ const series = ohlc
1394
+ ? pick(args, 3, ["close"], "null")
1395
+ : pick(args, 0, ["series", "source", "price"], "null");
1008
1396
  const i = ctx.plots.length;
1009
1397
  ctx.plots.push({ title });
1010
- return `plot_${i}[__bar_idx] = __h.naNum(${series})`;
1398
+ return `plot_${i}[__bar_idx] = __h.plotNum(${series})`;
1011
1399
  }
1012
1400
 
1013
1401
  function emitColor(name: string, args: CallArgs): string | null {
@@ -1084,9 +1472,13 @@ function emitMath(name: string, args: CallArgs): string | null {
1084
1472
  name === "todegrees" ||
1085
1473
  name === "toradians" ||
1086
1474
  name === "iff" ||
1087
- name === "fixnan";
1475
+ name === "fixnan" ||
1476
+ name === "round_to_mintick";
1088
1477
  if (!isNs && !isBareMath) return null;
1089
1478
 
1479
+ if (bare === "round_to_mintick") {
1480
+ return `__h.roundToMintick(${pick(args, 0, ["number", "x"], "null")})`;
1481
+ }
1090
1482
  if (bare === "iff") {
1091
1483
  const cond = pick(args, 0, ["condition", "cond"], "null");
1092
1484
  const a = pick(args, 1, ["then", "if_true"], "null");
@@ -1176,6 +1568,7 @@ function taCall(method: string, site: string, parts: string[]): string {
1176
1568
 
1177
1569
  function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1178
1570
  if (!TA_METHODS.has(method)) return "null";
1571
+ if (method === "ad") method = "accdist";
1179
1572
  const site = allocSite(ctx);
1180
1573
  const src = () => pick(args, 0, ["source", "series", "src"], CLOSE);
1181
1574
  const len = (index: number, fb: string, names: string[] = ["length", "len", "period"]) =>
@@ -1187,8 +1580,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1187
1580
  if (SRC_LEN1.has(method)) {
1188
1581
  return taCall(method, site, [src(), len(1, "1")]);
1189
1582
  }
1190
- if (method === "highest" || method === "lowest" || method === "max" || method === "min") {
1191
- const defSrc = method === "lowest" || method === "min" ? LOW : HIGH;
1583
+ if (method === "highest" || method === "lowest") {
1584
+ const defSrc = method === "lowest" ? LOW : HIGH;
1585
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
1586
+ // Python 1-arg period-only form (basic.py:222-246): source defaults to
1587
+ // the context high/low series and the length gates on _is_period_like
1588
+ // → non-period-like → na period 0 (interpret evalExtraTa).
1589
+ return taCall(method, site, [defSrc, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1590
+ }
1591
+ return taCall(method, site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1592
+ }
1593
+ if (method === "highestbars" || method === "lowestbars") {
1594
+ const defSrc = method === "lowestbars" ? LOW : HIGH;
1595
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
1596
+ // Python 1-arg period-only form (basic.py:248-291): context high/low
1597
+ // source, length gated like ta.highest (interpret evalExtraTa).
1598
+ return taCall(method, site, [defSrc, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1599
+ }
1600
+ return taCall(method, site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1601
+ }
1602
+ if (method === "max" || method === "min") {
1603
+ const defSrc = method === "min" ? LOW : HIGH;
1192
1604
  if (args.pos.length >= 2 || Object.hasOwn(args.kw, "source") || Object.hasOwn(args.kw, "series")) {
1193
1605
  return taCall(method, site, [src(), len(1, "14")]);
1194
1606
  }
@@ -1224,10 +1636,24 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1224
1636
  return `(() => { const __r = ${call}; return [__r.macd, __r.signal, __r.hist]; })()`;
1225
1637
  }
1226
1638
  if (method === "bb") {
1639
+ if (args.pos.length === 2 && !hasAnyKw(args, ["source", "series"])) {
1640
+ // Python period-first form (basic.py:394-401): (length, mult) with the
1641
+ // series defaulting to the context close. Dispatch gates slot 0 on
1642
+ // _is_period_like per bar (kwarg-first resolution, like interpret
1643
+ // callArg) — a non-period-like slot falls through to the series arm on
1644
+ // the raw slot-0 value, exactly like interpret.
1645
+ const p0Expr = pick(args, 0, ["source", "series", "length"], "null");
1646
+ const multPf = finiteOr(pick(args, 1, ["mult", "multiplier"], "null"), "NaN");
1647
+ const lenFt = finiteOr(pick(args, 1, ["length"], "null"), "0");
1648
+ const multFt = finiteOr(pick(args, 2, ["mult", "multiplier"], "null"), "2");
1649
+ const pf = `(() => { const __r = __h.ta.bb(${site}, close_arr[__bar_idx], __p0, ${multPf}); return [__r.mid, __r.up, __r.lo]; })()`;
1650
+ const ft = `(() => { const __r = __h.ta.bb(${site}, __s0, ${lenFt}, ${multFt}); return [__r.mid, __r.up, __r.lo]; })()`;
1651
+ return `((__p0, __s0) => (Number.isInteger(__p0) ? ${pf} : ${ft}))(${p0Expr}, ${pick(args, 0, ["source", "series"], "null")})`;
1652
+ }
1227
1653
  const call = taCall("bb", site, [
1228
1654
  src(),
1229
- len(1, "20"),
1230
- pick(args, 2, ["mult", "multiplier"], "2"),
1655
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1656
+ finiteOr(pick(args, 2, ["mult", "multiplier"], "null"), "2"),
1231
1657
  ]);
1232
1658
  return `(() => { const __r = ${call}; return [__r.mid, __r.up, __r.lo]; })()`;
1233
1659
  }
@@ -1245,21 +1671,52 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1245
1671
  return `(() => { const __r = ${call}; return [__r.mid, __r.up, __r.lo]; })()`;
1246
1672
  }
1247
1673
  if (method === "stoch") {
1248
- if (args.pos.length <= 1 && args.kw.high == null) {
1249
- return taCall("stoch", site, [CLOSE, HIGH, LOW, len(0, "14")]);
1674
+ if (args.kw.high == null && args.pos.length <= 2) {
1675
+ // Python period-only / community forms (oscillators.py:58-78): %K over
1676
+ // the context close/high/low. Both slots gate on _is_period_like; in
1677
+ // the 2-arg (kLength, dPeriod) form dPeriod is ignored (Python quirk).
1678
+ const length =
1679
+ args.pos.length === 2
1680
+ ? `(((__k, __d) => ((Number.isInteger(__k) && Number.isInteger(__d)) ? __k : 0))(${pick(args, 0, ["length"], "null")}, ${pick(args, 1, ["d", "smooth"], "null")}))`
1681
+ : periodGate(pick(args, 0, ["length"], "null"), "0");
1682
+ return taCall("stoch", site, [CLOSE, HIGH, LOW, length]);
1250
1683
  }
1251
1684
  return taCall("stoch", site, [
1252
1685
  src(),
1253
1686
  pick(args, 1, ["high"], HIGH),
1254
1687
  pick(args, 2, ["low"], LOW),
1255
- len(3, "14"),
1688
+ finiteOr(pick(args, 3, ["length"], "null"), "0"),
1256
1689
  ]);
1257
1690
  }
1258
1691
  if (method === "vwma") {
1259
- return taCall("vwma", site, [src(), pick(args, 2, ["volume"], VOL), len(1, "14")]);
1692
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["volume"])) {
1693
+ // Python 1-arg period-only form (basic.py:80-88): close + volume from
1694
+ // the chart context; length gates on _is_period_like → na period 0.
1695
+ return taCall("vwma", site, [CLOSE, VOL, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1696
+ }
1697
+ if (args.pos.length >= 3 || hasAnyKw(args, ["volume"])) {
1698
+ // Python 3-arg community form (basic.py:89-100): (source, volume,
1699
+ // length). A period-like middle slot swaps volume back to the context
1700
+ // series (basic.py:93/97) — mirror interpret's per-bar swap.
1701
+ const vol = `((__v) => (Number.isInteger(__v) ? ${VOL} : __v))(${pick(args, 1, ["volume"], "null")})`;
1702
+ return taCall("vwma", site, [src(), vol, finiteOr(pick(args, 2, ["length", "period"], "null"), "0")]);
1703
+ }
1704
+ return taCall("vwma", site, [src(), VOL, finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1260
1705
  }
1261
1706
  if (method === "cci") {
1262
- return taCall("cci", site, [src(), len(1, "14")]);
1707
+ if (args.pos.length >= 4) {
1708
+ // Python legacy 4-arg form (oscillators.py:178-187): typical price of
1709
+ // the given (high, low, close) series. Slots resolve kwarg-first.
1710
+ const tp = `((__h, __l, __c) => ((__h != null && __l != null && __c != null) ? (__h + __l + __c) / 3 : __c))(${pick(args, 0, ["high"], "null")}, ${pick(args, 1, ["low"], "null")}, ${pick(args, 2, ["close"], "null")})`;
1711
+ return taCall("cci", site, [tp, finiteOr(pick(args, 3, ["length"], "null"), "0")]);
1712
+ }
1713
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
1714
+ // Python 1-arg period-only form (oscillators.py:159-171): typical price
1715
+ // from the context high/low/close; length gates on _is_period_like.
1716
+ return taCall("cci", site, [TP, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1717
+ }
1718
+ // Python 2-arg form (oscillators.py:172-177): CCI over the source alone.
1719
+ return taCall("cci", site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1263
1720
  }
1264
1721
  if (method === "mfi") {
1265
1722
  if (args.pos.length <= 1) {
@@ -1336,10 +1793,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1336
1793
  ]);
1337
1794
  }
1338
1795
  if (method === "pivothigh" || method === "pivotlow") {
1796
+ if (args.pos.length === 2 && !hasAnyKw(args, ["source", "series"])) {
1797
+ // Python period-first form (basic.py:1007-1039): both slots gate on
1798
+ // _is_period_like and the source defaults to the context high/low
1799
+ // series. A non-period-like slot falls through to the series arm on
1800
+ // the raw slot values (never confirms on a scalar) — mirror interpret.
1801
+ // Slots resolve kwarg-first (leftbars/rightbars), like interpret callArg.
1802
+ const defSrc = method === "pivotlow" ? LOW : HIGH;
1803
+ const lExpr = pick(args, 0, ["leftbars", "left"], "null");
1804
+ const rExpr = pick(args, 1, ["rightbars", "right"], "null");
1805
+ const leftFt = finiteOr(pick(args, 1, ["leftbars", "left"], "null"), "5");
1806
+ const rightFt = finiteOr(pick(args, 2, ["rightbars", "right"], "null"), "5");
1807
+ const pf = taCall(method, site, [defSrc, "__l", "__r"]);
1808
+ const ft = taCall(method, site, ["__s", leftFt, rightFt]);
1809
+ return `((__l, __r, __s) => (((Number.isInteger(__l) && Number.isInteger(__r))) ? ${pf} : ${ft}))(${lExpr}, ${rExpr}, ${pick(args, 0, ["source", "series"], "null")})`;
1810
+ }
1811
+ // Interpret srcArg: source slot names ["source", "series"], missing → na
1812
+ // (a bare ta.pivothigh() never confirms, like interpret).
1339
1813
  return taCall(method, site, [
1340
- src(),
1341
- pick(args, 1, ["leftbars", "left"], "5"),
1342
- pick(args, 2, ["rightbars", "right"], "5"),
1814
+ pick(args, 0, ["source", "series"], "null"),
1815
+ finiteOr(pick(args, 1, ["leftbars", "left"], "null"), "5"),
1816
+ finiteOr(pick(args, 2, ["rightbars", "right"], "null"), "5"),
1343
1817
  ]);
1344
1818
  }
1345
1819
  if (method === "swma") {
@@ -1355,7 +1829,10 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1355
1829
  return taCall("willr", site, [HIGH, LOW, CLOSE, len(0, "14")]);
1356
1830
  }
1357
1831
  if (method === "vwap") {
1358
- return taCall("vwap", site, [src(), pick(args, 1, ["volume"], VOL)]);
1832
+ return taCall("vwap", site, [
1833
+ pick(args, 0, ["source", "series", "src"], HLC3),
1834
+ pick(args, 1, ["volume"], VOL),
1835
+ ]);
1359
1836
  }
1360
1837
  if (method === "sar") {
1361
1838
  return taCall("sar", site, [
@@ -1367,7 +1844,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1367
1844
  ]);
1368
1845
  }
1369
1846
  if (method === "adx") {
1370
- return taCall("adx", site, [HIGH, LOW, CLOSE, len(0, "14")]);
1847
+ if (args.pos.length >= 4) {
1848
+ // Python legacy 4-arg form (common.py:718-726): (high, low, close,
1849
+ // length) with explicit series. Slots resolve kwarg-first.
1850
+ return taCall("adx", site, [
1851
+ pick(args, 0, ["source", "series"], "null"),
1852
+ pick(args, 1, ["low"], "null"),
1853
+ pick(args, 2, ["close"], "null"),
1854
+ finiteOr(pick(args, 3, ["length"], "null"), "0"),
1855
+ ]);
1856
+ }
1857
+ if (args.pos.length === 2 && !hasAnyKw(args, ["high", "low", "close"])) {
1858
+ // Python 2-arg form (diLength, adxSmoothing) (common.py:709-717): both
1859
+ // slots gate on _is_period_like (kwarg-first resolution) and the ADX
1860
+ // period is the SECOND slot — diLength is accepted but unused. A
1861
+ // non-period-like slot → default 14.
1862
+ const period = `(((__di, __sm) => ((Number.isInteger(__di) && Number.isInteger(__sm)) ? __sm : 14))(${pick(args, 0, ["diLength", "length"], "null")}, ${pick(args, 1, ["adxSmoothing", "adxlen"], "null")}))`;
1863
+ return taCall("adx", site, [HIGH, LOW, CLOSE, period]);
1864
+ }
1865
+ // Python 1-arg period-only form gates length via _is_period_like
1866
+ // (common.py ~701): non-period-like → ignored → default 14 (as absent).
1867
+ return taCall("adx", site, [HIGH, LOW, CLOSE, periodGate(pick(args, 0, ["length"], "null"), "14")]);
1371
1868
  }
1372
1869
  if (method === "correlation") {
1373
1870
  return taCall("correlation", site, [
@@ -1387,20 +1884,216 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1387
1884
  pick(args, 3, ["volume"], VOL),
1388
1885
  ]);
1389
1886
  }
1390
- if (method === "pvt" || method === "obv" || method === "nvi" || method === "pvi") {
1391
- return taCall(method, site, [
1887
+ // `vpt` is an alias of the pvt kernel on interpret (no ta.vpt engine fn).
1888
+ if (method === "pvt" || method === "vpt" || method === "obv" || method === "nvi" || method === "pvi") {
1889
+ return taCall(method === "vpt" ? "pvt" : method, site, [
1392
1890
  pick(args, 0, ["source", "close"], CLOSE),
1393
1891
  pick(args, 1, ["volume"], VOL),
1394
1892
  ]);
1395
1893
  }
1396
1894
  if (method === "wad") {
1397
- return taCall("wad", site, [HIGH, LOW, CLOSE]);
1895
+ return taCall("wad", site, [
1896
+ pick(args, 0, ["high"], HIGH),
1897
+ pick(args, 1, ["low"], LOW),
1898
+ pick(args, 2, ["close"], CLOSE),
1899
+ pick(args, 3, ["volume"], VOL),
1900
+ ]);
1398
1901
  }
1399
1902
  if (method === "iii") {
1400
1903
  return taCall("iii", site, [HIGH, LOW, CLOSE, VOL]);
1401
1904
  }
1402
1905
  if (method === "wvad") {
1403
- return taCall("wvad", site, [OPEN, HIGH, LOW, CLOSE, VOL]);
1906
+ if (args.pos.length >= 4 || Object.hasOwn(args.kw, "high")) {
1907
+ return taCall("wvad", site, [
1908
+ pick(args, 0, ["high"], HIGH),
1909
+ pick(args, 1, ["low"], LOW),
1910
+ pick(args, 2, ["close"], CLOSE),
1911
+ pick(args, 3, ["volume"], VOL),
1912
+ pick(args, 4, ["length", "period"], "20"),
1913
+ ]);
1914
+ }
1915
+ return taCall("wvad", site, [HIGH, LOW, CLOSE, VOL, pick(args, 0, ["length", "period"], "20")]);
1916
+ }
1917
+ if (method === "cmf") {
1918
+ if (args.pos.length >= 5) {
1919
+ return taCall("cmf", site, [
1920
+ pick(args, 1, ["high"], HIGH),
1921
+ pick(args, 2, ["low"], LOW),
1922
+ pick(args, 0, ["close", "source"], CLOSE),
1923
+ pick(args, 3, ["volume"], VOL),
1924
+ pick(args, 4, ["length", "period"], "20"),
1925
+ ]);
1926
+ }
1927
+ return taCall("cmf", site, [HIGH, LOW, CLOSE, VOL, pick(args, 0, ["length", "period"], "20")]);
1928
+ }
1929
+ if (method === "klinger") {
1930
+ return taCall("klinger", site, [
1931
+ pick(args, 2, ["close"], CLOSE),
1932
+ pick(args, 3, ["volume"], VOL),
1933
+ pick(args, 4, ["fast_period", "fast", "fastlen"], "0"),
1934
+ pick(args, 5, ["slow_period", "slow", "slowlen"], "0"),
1935
+ ]);
1936
+ }
1937
+ if (method === "ao") {
1938
+ // SMA(hl2, fast) − SMA(hl2, slow); high/low always come from the bar context.
1939
+ return taCall("ao", site, [
1940
+ HIGH,
1941
+ LOW,
1942
+ pick(args, 0, ["fast", "fastlen"], "5"),
1943
+ pick(args, 1, ["slow", "slowlen"], "34"),
1944
+ ]);
1945
+ }
1946
+ if (method === "aroon") {
1947
+ // Kernel returns {down, up}; interpret exposes the tuple [down, up].
1948
+ const call = taCall("aroon", site, [HIGH, LOW, pick(args, 0, ["length"], "14")]);
1949
+ return `(() => { const __r = ${call}; return [__r.down, __r.up]; })()`;
1950
+ }
1951
+ if (method === "uo") {
1952
+ return taCall("uo", site, [
1953
+ HIGH,
1954
+ LOW,
1955
+ CLOSE,
1956
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1957
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1958
+ finiteOr(pick(args, 2, ["length"], "null"), "0"),
1959
+ ]);
1960
+ }
1961
+ if (method === "rci") {
1962
+ // srcArg: missing source is na, not close.
1963
+ return taCall("rci", site, [
1964
+ pick(args, 0, ["source", "series"], "null"),
1965
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1966
+ ]);
1967
+ }
1968
+ if (method === "dpo") {
1969
+ return taCall("dpo", site, [CLOSE, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
1970
+ }
1971
+ if (method === "kst") {
1972
+ return taCall("kst", site, [
1973
+ CLOSE,
1974
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1975
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1976
+ finiteOr(pick(args, 2, ["length"], "null"), "0"),
1977
+ finiteOr(pick(args, 3, ["length"], "null"), "0"),
1978
+ ]);
1979
+ }
1980
+ if (method === "stochrsi") {
1981
+ // Engine method is stochRsi; record already branded {stochrsi, signal}.
1982
+ return taCall("stochRsi", site, [
1983
+ CLOSE,
1984
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1985
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1986
+ ]);
1987
+ }
1988
+ if (method === "donchian") {
1989
+ return taCall("donchian", site, [HIGH, LOW, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
1990
+ }
1991
+ if (method === "ichimoku") {
1992
+ return taCall("ichimoku", site, [
1993
+ HIGH,
1994
+ LOW,
1995
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1996
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1997
+ ]);
1998
+ }
1999
+ if (method === "bb_pct") {
2000
+ return taCall("bbPct", site, [
2001
+ CLOSE,
2002
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
2003
+ finiteOr(pick(args, 1, ["std_dev", "mult", "std"], "null"), "2"),
2004
+ ]);
2005
+ }
2006
+ if (method === "emv") {
2007
+ return taCall("emv", site, [HIGH, LOW, VOL, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
2008
+ }
2009
+ if (method === "fractal") {
2010
+ return taCall("fractal", site, [HIGH, LOW, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
2011
+ }
2012
+ if (method === "atr_stop") {
2013
+ // Kernel has no call-site; missing multiplier → 2 (interpret numArg).
2014
+ return `__h.ta.atrStop(${CLOSE}, ${pick(args, 0, ["atr", "atr_value"], "null")}, ${finiteOr(pick(args, 1, ["multiplier", "mult"], "null"), "2")})`;
2015
+ }
2016
+ if (method === "zigzag") {
2017
+ const call = taCall("zigzag", site, [
2018
+ pick(args, 0, ["source", "series"], "null"),
2019
+ finiteOr(pick(args, 1, ["threshold", "percent", "deviation"], "null"), "5"),
2020
+ ]);
2021
+ return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
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
+ ]);
1404
2097
  }
1405
2098
  return "null";
1406
2099
  }