@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.
@@ -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,40 @@ 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
+ ]);
140
+
141
+ /**
142
+ * Bare `ta.<attr>` auto-call whitelist. Mirrors interpret's evalTaAttr
143
+ * (interpret.ts) exactly: only these attributes auto-invoke a zero-arg TA
144
+ * kernel on a bare attribute load; anything else is na on both backends.
145
+ * `aroon` is deliberately absent — interpret's evalTaAttr does not handle it.
146
+ */
147
+ export const TA_BARE_ATTRS = new Set([
148
+ "accdist",
149
+ "ad",
150
+ "obv",
151
+ "tr",
152
+ "vwap",
153
+ "pvt",
154
+ "vpt",
155
+ "nvi",
156
+ "pvi",
157
+ "wad",
158
+ "wvad",
159
+ "cmf",
160
+ "ao",
109
161
  ]);
110
162
 
111
163
  const SRC_LEN = new Set([
@@ -120,8 +172,6 @@ const SRC_LEN = new Set([
120
172
  "sum",
121
173
  "dema",
122
174
  "tema",
123
- "highestbars",
124
- "lowestbars",
125
175
  "dev",
126
176
  "variance",
127
177
  "median",
@@ -141,6 +191,8 @@ const PLOT_FUNCS = new Set([
141
191
  "hline",
142
192
  "bgcolor",
143
193
  "barcolor",
194
+ "plotcandle",
195
+ "plotbar",
144
196
  ]);
145
197
 
146
198
  const STRATEGY_ACTIONS = new Set([
@@ -160,6 +212,10 @@ const STRATEGY_QUERIES = new Set([
160
212
  "strategy_openprofit",
161
213
  "strategy_opentrades",
162
214
  "strategy_closedtrades",
215
+ "strategy_leverage",
216
+ "strategy_margin_liquidation_price",
217
+ "strategy_position_avg_price",
218
+ "strategy_initial_capital",
163
219
  ]);
164
220
 
165
221
  const CHART_SERIES_ARR: Record<string, string> = {
@@ -232,7 +288,7 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
232
288
  if (PLOT_FUNCS.has(name)) return emitPlot(ctx, name, args);
233
289
 
234
290
  if (name === "indicator" || name === "study" || name === "library") return "null";
235
- if (name === "strategy") return "null";
291
+ if (name === "strategy") return emitStrategyDecl(ctx, args);
236
292
 
237
293
  if (name === "na") {
238
294
  if (args.pos.length === 0 && args.kw.x == null && args.kw.source == null) return "null";
@@ -275,6 +331,19 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
275
331
  }
276
332
  return "86400";
277
333
  }
334
+ if (name === "timeframe_change") {
335
+ // Prev bar time is `time_arr[__bar_idx-1]`; bar 0 has no prev (interpret series.get(1)).
336
+ return `__h.timeframeChange(${pick(args, 0, ["timeframe", "tf"], "null")}, time, (__bar_idx > 0 ? time_arr[__bar_idx - 1] : null), __bar_idx)`;
337
+ }
338
+ if (name === "time_close") {
339
+ return `__h.timeClose(__bar_idx, time_arr)`;
340
+ }
341
+ if (name === "syminfo_prefix") {
342
+ if (hasArg(args, 0, ["tickerid", "symbol"])) {
343
+ return `__h.syminfoPrefix(${pick(args, 0, ["tickerid", "symbol"], "null")})`;
344
+ }
345
+ return `__h.syminfoPrefix()`;
346
+ }
278
347
  if (name === "ticker_heikinashi" || name === "heikinashi") {
279
348
  ctx.needsHeikinashi = true;
280
349
  return `"__HEIKINASHI__"`;
@@ -432,6 +501,30 @@ function hasArg(args: CallArgs, index: number, names: string[]): boolean {
432
501
  return index >= 0 && index < args.pos.length;
433
502
  }
434
503
 
504
+ /** True when a keyword argument named one of *names* is present (interpret hasKwarg). */
505
+ function hasAnyKw(args: CallArgs, names: string[]): boolean {
506
+ return names.some((n) => Object.hasOwn(args.kw, n));
507
+ }
508
+
509
+ /**
510
+ * Runtime-safe `_is_period_like` gate (interpret `periodLikeCell`): only whole
511
+ * numbers pass and are returned as-is; everything else (na, fractional,
512
+ * series samples) collapses to *fb* — mirroring `periodLikeArg(...) ?? fb`.
513
+ * Constant args could be decided at emit time, but the runtime form is
514
+ * equally exact and keeps one code path for constants and series alike.
515
+ */
516
+ function periodGate(expr: string, fb: string): string {
517
+ return `((__p) => (Number.isInteger(__p) ? __p : ${fb}))(${expr})`;
518
+ }
519
+
520
+ /**
521
+ * Interpret `lenArg` / `lenOrDefault` analog: a null (na) or non-finite slot
522
+ * collapses to *fb* instead of reaching the kernel raw.
523
+ */
524
+ function finiteOr(expr: string, fb: string): string {
525
+ return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
526
+ }
527
+
435
528
  const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
436
529
 
437
530
  const ARRAY_SIGS: Record<string, string[][]> = {
@@ -452,7 +545,7 @@ const ARRAY_SIGS: Record<string, string[][]> = {
452
545
  fill: [["id"], ["value"]],
453
546
  copy: [["id"]],
454
547
  reverse: [["id"]],
455
- sort: [["id"], ["order"]],
548
+ sort: [["id"], ["order"], ["sort_field"]],
456
549
  concat: [["id"], ["other", "id2"]],
457
550
  slice: [["id"], ["index_from", "from"], ["index_to", "to"]],
458
551
  indexof: [["id"], ["value"]],
@@ -476,10 +569,10 @@ const ARRAY_SIGS: Record<string, string[][]> = {
476
569
  percentile_linear_interpolation: [["id"], ["percentage", "percent"]],
477
570
  percentile_nearest: [["id"], ["percentage", "percent"]],
478
571
  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"]],
572
+ binary_search: [["id"], ["value"], ["sort_field"]],
573
+ binary_search_leftmost: [["id"], ["value"], ["sort_field"]],
574
+ binary_search_rightmost: [["id"], ["value"], ["sort_field"]],
575
+ sort_indices: [["id"], ["order"], ["sort_field"]],
483
576
  covariance: [["id"], ["id2", "other"], ["biased"]],
484
577
  };
485
578
 
@@ -541,7 +634,8 @@ const MATRIX_SIGS: Record<string, string[][]> = {
541
634
  concat: [["id"], ["other"]],
542
635
  reshape: [["id"], ["rows"], ["columns", "cols"]],
543
636
  reverse: [["id"]],
544
- sort: [["id"], ["column", "col"], ["order"]],
637
+ sort: [["id"], ["column", "col"], ["order"], ["sort_field"]],
638
+ sort_indices: [["id"], ["column", "col"], ["order"], ["sort_field"]],
545
639
  eigenvalues: [["id"]],
546
640
  eigenvectors: [["id"]],
547
641
  pow: [["id"], ["power", "n"]],
@@ -554,6 +648,26 @@ const MATRIX_SIGS: Record<string, string[][]> = {
554
648
  remove_col: [["id"], ["column", "index"]],
555
649
  remove_column: [["id"], ["column", "index"]],
556
650
  submatrix: [["id"], ["from_row"], ["to_row"], ["from_column", "from_col"], ["to_column", "to_col"]],
651
+ sum_row: [["id"], ["row", "index"]],
652
+ avg_row: [["id"], ["row", "index"]],
653
+ min_row: [["id"], ["row", "index"]],
654
+ max_row: [["id"], ["row", "index"]],
655
+ mode_row: [["id"], ["row", "index"]],
656
+ sum_col: [["id"], ["column", "col", "index"]],
657
+ avg_col: [["id"], ["column", "col", "index"]],
658
+ min_col: [["id"], ["column", "col", "index"]],
659
+ max_col: [["id"], ["column", "col", "index"]],
660
+ mode_col: [["id"], ["column", "col", "index"]],
661
+ copy_row: [["id"], ["row", "index"]],
662
+ copy_col: [["id"], ["column", "col", "index"]],
663
+ fill_row: [["id"], ["row", "index"], ["value"]],
664
+ fill_col: [["id"], ["column", "col", "index"], ["value"]],
665
+ fill_diagonal: [["id"], ["value"]],
666
+ reverse_rows: [["id"]],
667
+ reverse_cols: [["id"]],
668
+ stdev: [["id"]],
669
+ variance: [["id"]],
670
+ mode_all: [["id"]],
557
671
  };
558
672
 
559
673
  function nsMethod(name: string, prefix: string): string | null {
@@ -562,6 +676,8 @@ function nsMethod(name: string, prefix: string): string | null {
562
676
  }
563
677
 
564
678
  function ctorMethod(method: string): string {
679
+ // Every Python `array.new*` spelling (newint, new_float, new_chart.point, …)
680
+ // lowers to the same PineArray constructor as `array.new`.
565
681
  return method === "from" || method.startsWith("new") ? "new" : method;
566
682
  }
567
683
 
@@ -891,6 +1007,26 @@ function isStrategyAction(name: string, method: string): boolean {
891
1007
  );
892
1008
  }
893
1009
 
1010
+ /** `strategy(...)` declaration — kwargs only, matching interpret `applyStrategyDecl`. */
1011
+ function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
1012
+ ctx.usesStrategy = true;
1013
+ const fields: string[] = [];
1014
+ const add = (names: string[], key: string): void => {
1015
+ if (hasArg(args, -1, names)) fields.push(`${key}: ${pick(args, -1, names, "null")}`);
1016
+ };
1017
+ add(["commission", "commission_value"], "commission");
1018
+ add(["slippage"], "slippage");
1019
+ add(["pyramiding"], "pyramiding");
1020
+ add(["avg_price_model"], "avg_price_model");
1021
+ add(["leverage"], "leverage");
1022
+ add(["margin_long"], "margin_long");
1023
+ add(["margin_short"], "margin_short");
1024
+ add(["default_qty_type"], "default_qty_type");
1025
+ add(["default_qty_value"], "default_qty_value");
1026
+ add(["initial_capital"], "initial_capital");
1027
+ return `__h.strategy.configure({${fields.join(", ")}})`;
1028
+ }
1029
+
894
1030
  function emitStrategyEntry(method: "entry" | "order", args: CallArgs): string {
895
1031
  const id = pick(args, 0, ["id"], "null");
896
1032
  const direction = pick(args, 1, ["direction"], "null");
@@ -926,10 +1062,22 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
926
1062
  return "__h.strategy.close_all()";
927
1063
  case "exit": {
928
1064
  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})`;
1065
+ const id = pick(args, 0, ["id"], "null");
1066
+ const fields: string[] = [];
1067
+ const add = (index: number, names: string[], key: string): void => {
1068
+ if (hasArg(args, index, names)) fields.push(`${key}: ${pick(args, index, names, "null")}`);
1069
+ };
1070
+ add(1, ["from_entry"], "from_entry");
1071
+ add(2, ["qty"], "qty");
1072
+ add(3, ["qty_percent"], "qty_percent");
1073
+ add(4, ["profit"], "profit");
1074
+ add(5, ["limit"], "limit");
1075
+ add(6, ["loss"], "loss");
1076
+ add(7, ["stop"], "stop");
1077
+ add(8, ["trail_price"], "trail_price");
1078
+ add(9, ["trail_points"], "trail_points");
1079
+ add(10, ["trail_offset"], "trail_offset");
1080
+ return `__h.strategy.exit(${id}, {${fields.join(", ")}})`;
933
1081
  }
934
1082
  case "cancel":
935
1083
  ctx.usesStrategy = true;
@@ -943,6 +1091,10 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
943
1091
  case "openprofit":
944
1092
  case "opentrades":
945
1093
  case "closedtrades":
1094
+ case "leverage":
1095
+ case "margin_liquidation_price":
1096
+ case "position_avg_price":
1097
+ case "initial_capital":
946
1098
  ctx.usesStrategy = true;
947
1099
  return `__h.strategy.${method}()`;
948
1100
  case "risk_allow_entry_in":
@@ -992,7 +1144,7 @@ function packUserFuncArgs(ctx: EmitCtx, name: string, args: CallArgs): string[]
992
1144
  return packed;
993
1145
  }
994
1146
 
995
- function emitPlot(ctx: EmitCtx, _fname: string, args: CallArgs): string {
1147
+ function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
996
1148
  let title: string | null = constString(args.kwNodes.title);
997
1149
  if (title == null) {
998
1150
  for (const n of args.posNodes) {
@@ -1003,8 +1155,16 @@ function emitPlot(ctx: EmitCtx, _fname: string, args: CallArgs): string {
1003
1155
  }
1004
1156
  }
1005
1157
  }
1006
- if (title == null || title === "") title = `plot_${ctx.plots.length}`;
1007
- const series = pick(args, 0, ["series", "source", "price"], "null");
1158
+ if (title == null || title === "") {
1159
+ if (fname === "plotcandle") title = "candles";
1160
+ else if (fname === "plotbar") title = "bars";
1161
+ else title = `plot_${ctx.plots.length}`;
1162
+ }
1163
+ // Python primary column for plotcandle/plotbar is close (arg 3), not open.
1164
+ const ohlc = fname === "plotcandle" || fname === "plotbar";
1165
+ const series = ohlc
1166
+ ? pick(args, 3, ["close"], "null")
1167
+ : pick(args, 0, ["series", "source", "price"], "null");
1008
1168
  const i = ctx.plots.length;
1009
1169
  ctx.plots.push({ title });
1010
1170
  return `plot_${i}[__bar_idx] = __h.naNum(${series})`;
@@ -1084,9 +1244,13 @@ function emitMath(name: string, args: CallArgs): string | null {
1084
1244
  name === "todegrees" ||
1085
1245
  name === "toradians" ||
1086
1246
  name === "iff" ||
1087
- name === "fixnan";
1247
+ name === "fixnan" ||
1248
+ name === "round_to_mintick";
1088
1249
  if (!isNs && !isBareMath) return null;
1089
1250
 
1251
+ if (bare === "round_to_mintick") {
1252
+ return `__h.roundToMintick(${pick(args, 0, ["number", "x"], "null")})`;
1253
+ }
1090
1254
  if (bare === "iff") {
1091
1255
  const cond = pick(args, 0, ["condition", "cond"], "null");
1092
1256
  const a = pick(args, 1, ["then", "if_true"], "null");
@@ -1176,6 +1340,7 @@ function taCall(method: string, site: string, parts: string[]): string {
1176
1340
 
1177
1341
  function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1178
1342
  if (!TA_METHODS.has(method)) return "null";
1343
+ if (method === "ad") method = "accdist";
1179
1344
  const site = allocSite(ctx);
1180
1345
  const src = () => pick(args, 0, ["source", "series", "src"], CLOSE);
1181
1346
  const len = (index: number, fb: string, names: string[] = ["length", "len", "period"]) =>
@@ -1187,8 +1352,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1187
1352
  if (SRC_LEN1.has(method)) {
1188
1353
  return taCall(method, site, [src(), len(1, "1")]);
1189
1354
  }
1190
- if (method === "highest" || method === "lowest" || method === "max" || method === "min") {
1191
- const defSrc = method === "lowest" || method === "min" ? LOW : HIGH;
1355
+ if (method === "highest" || method === "lowest") {
1356
+ const defSrc = method === "lowest" ? LOW : HIGH;
1357
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
1358
+ // Python 1-arg period-only form (basic.py:222-246): source defaults to
1359
+ // the context high/low series and the length gates on _is_period_like
1360
+ // → non-period-like → na period 0 (interpret evalExtraTa).
1361
+ return taCall(method, site, [defSrc, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1362
+ }
1363
+ return taCall(method, site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1364
+ }
1365
+ if (method === "highestbars" || method === "lowestbars") {
1366
+ const defSrc = method === "lowestbars" ? LOW : HIGH;
1367
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
1368
+ // Python 1-arg period-only form (basic.py:248-291): context high/low
1369
+ // source, length gated like ta.highest (interpret evalExtraTa).
1370
+ return taCall(method, site, [defSrc, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1371
+ }
1372
+ return taCall(method, site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1373
+ }
1374
+ if (method === "max" || method === "min") {
1375
+ const defSrc = method === "min" ? LOW : HIGH;
1192
1376
  if (args.pos.length >= 2 || Object.hasOwn(args.kw, "source") || Object.hasOwn(args.kw, "series")) {
1193
1377
  return taCall(method, site, [src(), len(1, "14")]);
1194
1378
  }
@@ -1224,10 +1408,24 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1224
1408
  return `(() => { const __r = ${call}; return [__r.macd, __r.signal, __r.hist]; })()`;
1225
1409
  }
1226
1410
  if (method === "bb") {
1411
+ if (args.pos.length === 2 && !hasAnyKw(args, ["source", "series"])) {
1412
+ // Python period-first form (basic.py:394-401): (length, mult) with the
1413
+ // series defaulting to the context close. Dispatch gates slot 0 on
1414
+ // _is_period_like per bar (kwarg-first resolution, like interpret
1415
+ // callArg) — a non-period-like slot falls through to the series arm on
1416
+ // the raw slot-0 value, exactly like interpret.
1417
+ const p0Expr = pick(args, 0, ["source", "series", "length"], "null");
1418
+ const multPf = finiteOr(pick(args, 1, ["mult", "multiplier"], "null"), "NaN");
1419
+ const lenFt = finiteOr(pick(args, 1, ["length"], "null"), "0");
1420
+ const multFt = finiteOr(pick(args, 2, ["mult", "multiplier"], "null"), "2");
1421
+ const pf = `(() => { const __r = __h.ta.bb(${site}, close_arr[__bar_idx], __p0, ${multPf}); return [__r.mid, __r.up, __r.lo]; })()`;
1422
+ const ft = `(() => { const __r = __h.ta.bb(${site}, __s0, ${lenFt}, ${multFt}); return [__r.mid, __r.up, __r.lo]; })()`;
1423
+ return `((__p0, __s0) => (Number.isInteger(__p0) ? ${pf} : ${ft}))(${p0Expr}, ${pick(args, 0, ["source", "series"], "null")})`;
1424
+ }
1227
1425
  const call = taCall("bb", site, [
1228
1426
  src(),
1229
- len(1, "20"),
1230
- pick(args, 2, ["mult", "multiplier"], "2"),
1427
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1428
+ finiteOr(pick(args, 2, ["mult", "multiplier"], "null"), "2"),
1231
1429
  ]);
1232
1430
  return `(() => { const __r = ${call}; return [__r.mid, __r.up, __r.lo]; })()`;
1233
1431
  }
@@ -1245,21 +1443,52 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1245
1443
  return `(() => { const __r = ${call}; return [__r.mid, __r.up, __r.lo]; })()`;
1246
1444
  }
1247
1445
  if (method === "stoch") {
1248
- if (args.pos.length <= 1 && args.kw.high == null) {
1249
- return taCall("stoch", site, [CLOSE, HIGH, LOW, len(0, "14")]);
1446
+ if (args.kw.high == null && args.pos.length <= 2) {
1447
+ // Python period-only / community forms (oscillators.py:58-78): %K over
1448
+ // the context close/high/low. Both slots gate on _is_period_like; in
1449
+ // the 2-arg (kLength, dPeriod) form dPeriod is ignored (Python quirk).
1450
+ const length =
1451
+ args.pos.length === 2
1452
+ ? `(((__k, __d) => ((Number.isInteger(__k) && Number.isInteger(__d)) ? __k : 0))(${pick(args, 0, ["length"], "null")}, ${pick(args, 1, ["d", "smooth"], "null")}))`
1453
+ : periodGate(pick(args, 0, ["length"], "null"), "0");
1454
+ return taCall("stoch", site, [CLOSE, HIGH, LOW, length]);
1250
1455
  }
1251
1456
  return taCall("stoch", site, [
1252
1457
  src(),
1253
1458
  pick(args, 1, ["high"], HIGH),
1254
1459
  pick(args, 2, ["low"], LOW),
1255
- len(3, "14"),
1460
+ finiteOr(pick(args, 3, ["length"], "null"), "0"),
1256
1461
  ]);
1257
1462
  }
1258
1463
  if (method === "vwma") {
1259
- return taCall("vwma", site, [src(), pick(args, 2, ["volume"], VOL), len(1, "14")]);
1464
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["volume"])) {
1465
+ // Python 1-arg period-only form (basic.py:80-88): close + volume from
1466
+ // the chart context; length gates on _is_period_like → na period 0.
1467
+ return taCall("vwma", site, [CLOSE, VOL, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1468
+ }
1469
+ if (args.pos.length >= 3 || hasAnyKw(args, ["volume"])) {
1470
+ // Python 3-arg community form (basic.py:89-100): (source, volume,
1471
+ // length). A period-like middle slot swaps volume back to the context
1472
+ // series (basic.py:93/97) — mirror interpret's per-bar swap.
1473
+ const vol = `((__v) => (Number.isInteger(__v) ? ${VOL} : __v))(${pick(args, 1, ["volume"], "null")})`;
1474
+ return taCall("vwma", site, [src(), vol, finiteOr(pick(args, 2, ["length", "period"], "null"), "0")]);
1475
+ }
1476
+ return taCall("vwma", site, [src(), VOL, finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1260
1477
  }
1261
1478
  if (method === "cci") {
1262
- return taCall("cci", site, [src(), len(1, "14")]);
1479
+ if (args.pos.length >= 4) {
1480
+ // Python legacy 4-arg form (oscillators.py:178-187): typical price of
1481
+ // the given (high, low, close) series. Slots resolve kwarg-first.
1482
+ 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")})`;
1483
+ return taCall("cci", site, [tp, finiteOr(pick(args, 3, ["length"], "null"), "0")]);
1484
+ }
1485
+ if (args.pos.length <= 1 && !hasAnyKw(args, ["source", "series"])) {
1486
+ // Python 1-arg period-only form (oscillators.py:159-171): typical price
1487
+ // from the context high/low/close; length gates on _is_period_like.
1488
+ return taCall("cci", site, [TP, periodGate(pick(args, 0, ["length"], "null"), "0")]);
1489
+ }
1490
+ // Python 2-arg form (oscillators.py:172-177): CCI over the source alone.
1491
+ return taCall("cci", site, [src(), finiteOr(pick(args, 1, ["length"], "null"), "0")]);
1263
1492
  }
1264
1493
  if (method === "mfi") {
1265
1494
  if (args.pos.length <= 1) {
@@ -1336,10 +1565,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1336
1565
  ]);
1337
1566
  }
1338
1567
  if (method === "pivothigh" || method === "pivotlow") {
1568
+ if (args.pos.length === 2 && !hasAnyKw(args, ["source", "series"])) {
1569
+ // Python period-first form (basic.py:1007-1039): both slots gate on
1570
+ // _is_period_like and the source defaults to the context high/low
1571
+ // series. A non-period-like slot falls through to the series arm on
1572
+ // the raw slot values (never confirms on a scalar) — mirror interpret.
1573
+ // Slots resolve kwarg-first (leftbars/rightbars), like interpret callArg.
1574
+ const defSrc = method === "pivotlow" ? LOW : HIGH;
1575
+ const lExpr = pick(args, 0, ["leftbars", "left"], "null");
1576
+ const rExpr = pick(args, 1, ["rightbars", "right"], "null");
1577
+ const leftFt = finiteOr(pick(args, 1, ["leftbars", "left"], "null"), "5");
1578
+ const rightFt = finiteOr(pick(args, 2, ["rightbars", "right"], "null"), "5");
1579
+ const pf = taCall(method, site, [defSrc, "__l", "__r"]);
1580
+ const ft = taCall(method, site, ["__s", leftFt, rightFt]);
1581
+ return `((__l, __r, __s) => (((Number.isInteger(__l) && Number.isInteger(__r))) ? ${pf} : ${ft}))(${lExpr}, ${rExpr}, ${pick(args, 0, ["source", "series"], "null")})`;
1582
+ }
1583
+ // Interpret srcArg: source slot names ["source", "series"], missing → na
1584
+ // (a bare ta.pivothigh() never confirms, like interpret).
1339
1585
  return taCall(method, site, [
1340
- src(),
1341
- pick(args, 1, ["leftbars", "left"], "5"),
1342
- pick(args, 2, ["rightbars", "right"], "5"),
1586
+ pick(args, 0, ["source", "series"], "null"),
1587
+ finiteOr(pick(args, 1, ["leftbars", "left"], "null"), "5"),
1588
+ finiteOr(pick(args, 2, ["rightbars", "right"], "null"), "5"),
1343
1589
  ]);
1344
1590
  }
1345
1591
  if (method === "swma") {
@@ -1355,7 +1601,10 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1355
1601
  return taCall("willr", site, [HIGH, LOW, CLOSE, len(0, "14")]);
1356
1602
  }
1357
1603
  if (method === "vwap") {
1358
- return taCall("vwap", site, [src(), pick(args, 1, ["volume"], VOL)]);
1604
+ return taCall("vwap", site, [
1605
+ pick(args, 0, ["source", "series", "src"], HLC3),
1606
+ pick(args, 1, ["volume"], VOL),
1607
+ ]);
1359
1608
  }
1360
1609
  if (method === "sar") {
1361
1610
  return taCall("sar", site, [
@@ -1367,7 +1616,27 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1367
1616
  ]);
1368
1617
  }
1369
1618
  if (method === "adx") {
1370
- return taCall("adx", site, [HIGH, LOW, CLOSE, len(0, "14")]);
1619
+ if (args.pos.length >= 4) {
1620
+ // Python legacy 4-arg form (common.py:718-726): (high, low, close,
1621
+ // length) with explicit series. Slots resolve kwarg-first.
1622
+ return taCall("adx", site, [
1623
+ pick(args, 0, ["source", "series"], "null"),
1624
+ pick(args, 1, ["low"], "null"),
1625
+ pick(args, 2, ["close"], "null"),
1626
+ finiteOr(pick(args, 3, ["length"], "null"), "0"),
1627
+ ]);
1628
+ }
1629
+ if (args.pos.length === 2 && !hasAnyKw(args, ["high", "low", "close"])) {
1630
+ // Python 2-arg form (diLength, adxSmoothing) (common.py:709-717): both
1631
+ // slots gate on _is_period_like (kwarg-first resolution) and the ADX
1632
+ // period is the SECOND slot — diLength is accepted but unused. A
1633
+ // non-period-like slot → default 14.
1634
+ const period = `(((__di, __sm) => ((Number.isInteger(__di) && Number.isInteger(__sm)) ? __sm : 14))(${pick(args, 0, ["diLength", "length"], "null")}, ${pick(args, 1, ["adxSmoothing", "adxlen"], "null")}))`;
1635
+ return taCall("adx", site, [HIGH, LOW, CLOSE, period]);
1636
+ }
1637
+ // Python 1-arg period-only form gates length via _is_period_like
1638
+ // (common.py ~701): non-period-like → ignored → default 14 (as absent).
1639
+ return taCall("adx", site, [HIGH, LOW, CLOSE, periodGate(pick(args, 0, ["length"], "null"), "14")]);
1371
1640
  }
1372
1641
  if (method === "correlation") {
1373
1642
  return taCall("correlation", site, [
@@ -1387,20 +1656,141 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1387
1656
  pick(args, 3, ["volume"], VOL),
1388
1657
  ]);
1389
1658
  }
1390
- if (method === "pvt" || method === "obv" || method === "nvi" || method === "pvi") {
1391
- return taCall(method, site, [
1659
+ // `vpt` is an alias of the pvt kernel on interpret (no ta.vpt engine fn).
1660
+ if (method === "pvt" || method === "vpt" || method === "obv" || method === "nvi" || method === "pvi") {
1661
+ return taCall(method === "vpt" ? "pvt" : method, site, [
1392
1662
  pick(args, 0, ["source", "close"], CLOSE),
1393
1663
  pick(args, 1, ["volume"], VOL),
1394
1664
  ]);
1395
1665
  }
1396
1666
  if (method === "wad") {
1397
- return taCall("wad", site, [HIGH, LOW, CLOSE]);
1667
+ return taCall("wad", site, [
1668
+ pick(args, 0, ["high"], HIGH),
1669
+ pick(args, 1, ["low"], LOW),
1670
+ pick(args, 2, ["close"], CLOSE),
1671
+ pick(args, 3, ["volume"], VOL),
1672
+ ]);
1398
1673
  }
1399
1674
  if (method === "iii") {
1400
1675
  return taCall("iii", site, [HIGH, LOW, CLOSE, VOL]);
1401
1676
  }
1402
1677
  if (method === "wvad") {
1403
- return taCall("wvad", site, [OPEN, HIGH, LOW, CLOSE, VOL]);
1678
+ if (args.pos.length >= 4 || Object.hasOwn(args.kw, "high")) {
1679
+ return taCall("wvad", site, [
1680
+ pick(args, 0, ["high"], HIGH),
1681
+ pick(args, 1, ["low"], LOW),
1682
+ pick(args, 2, ["close"], CLOSE),
1683
+ pick(args, 3, ["volume"], VOL),
1684
+ pick(args, 4, ["length", "period"], "20"),
1685
+ ]);
1686
+ }
1687
+ return taCall("wvad", site, [HIGH, LOW, CLOSE, VOL, pick(args, 0, ["length", "period"], "20")]);
1688
+ }
1689
+ if (method === "cmf") {
1690
+ if (args.pos.length >= 5) {
1691
+ return taCall("cmf", site, [
1692
+ pick(args, 1, ["high"], HIGH),
1693
+ pick(args, 2, ["low"], LOW),
1694
+ pick(args, 0, ["close", "source"], CLOSE),
1695
+ pick(args, 3, ["volume"], VOL),
1696
+ pick(args, 4, ["length", "period"], "20"),
1697
+ ]);
1698
+ }
1699
+ return taCall("cmf", site, [HIGH, LOW, CLOSE, VOL, pick(args, 0, ["length", "period"], "20")]);
1700
+ }
1701
+ if (method === "klinger") {
1702
+ return taCall("klinger", site, [
1703
+ pick(args, 2, ["close"], CLOSE),
1704
+ pick(args, 3, ["volume"], VOL),
1705
+ pick(args, 4, ["fast_period", "fast", "fastlen"], "0"),
1706
+ pick(args, 5, ["slow_period", "slow", "slowlen"], "0"),
1707
+ ]);
1708
+ }
1709
+ if (method === "ao") {
1710
+ // SMA(hl2, fast) − SMA(hl2, slow); high/low always come from the bar context.
1711
+ return taCall("ao", site, [
1712
+ HIGH,
1713
+ LOW,
1714
+ pick(args, 0, ["fast", "fastlen"], "5"),
1715
+ pick(args, 1, ["slow", "slowlen"], "34"),
1716
+ ]);
1717
+ }
1718
+ if (method === "aroon") {
1719
+ // Kernel returns {down, up}; interpret exposes the tuple [down, up].
1720
+ const call = taCall("aroon", site, [HIGH, LOW, pick(args, 0, ["length"], "14")]);
1721
+ return `(() => { const __r = ${call}; return [__r.down, __r.up]; })()`;
1722
+ }
1723
+ if (method === "uo") {
1724
+ return taCall("uo", site, [
1725
+ HIGH,
1726
+ LOW,
1727
+ CLOSE,
1728
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1729
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1730
+ finiteOr(pick(args, 2, ["length"], "null"), "0"),
1731
+ ]);
1732
+ }
1733
+ if (method === "rci") {
1734
+ // srcArg: missing source is na, not close.
1735
+ return taCall("rci", site, [
1736
+ pick(args, 0, ["source", "series"], "null"),
1737
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1738
+ ]);
1739
+ }
1740
+ if (method === "dpo") {
1741
+ return taCall("dpo", site, [CLOSE, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
1742
+ }
1743
+ if (method === "kst") {
1744
+ return taCall("kst", site, [
1745
+ CLOSE,
1746
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1747
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1748
+ finiteOr(pick(args, 2, ["length"], "null"), "0"),
1749
+ finiteOr(pick(args, 3, ["length"], "null"), "0"),
1750
+ ]);
1751
+ }
1752
+ if (method === "stochrsi") {
1753
+ // Engine method is stochRsi; record already branded {stochrsi, signal}.
1754
+ return taCall("stochRsi", site, [
1755
+ CLOSE,
1756
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1757
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1758
+ ]);
1759
+ }
1760
+ if (method === "donchian") {
1761
+ return taCall("donchian", site, [HIGH, LOW, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
1762
+ }
1763
+ if (method === "ichimoku") {
1764
+ return taCall("ichimoku", site, [
1765
+ HIGH,
1766
+ LOW,
1767
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1768
+ finiteOr(pick(args, 1, ["length"], "null"), "0"),
1769
+ ]);
1770
+ }
1771
+ if (method === "bb_pct") {
1772
+ return taCall("bbPct", site, [
1773
+ CLOSE,
1774
+ finiteOr(pick(args, 0, ["length"], "null"), "0"),
1775
+ finiteOr(pick(args, 1, ["std_dev", "mult", "std"], "null"), "2"),
1776
+ ]);
1777
+ }
1778
+ if (method === "emv") {
1779
+ return taCall("emv", site, [HIGH, LOW, VOL, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
1780
+ }
1781
+ if (method === "fractal") {
1782
+ return taCall("fractal", site, [HIGH, LOW, finiteOr(pick(args, 0, ["length"], "null"), "0")]);
1783
+ }
1784
+ if (method === "atr_stop") {
1785
+ // Kernel has no call-site; missing multiplier → 2 (interpret numArg).
1786
+ return `__h.ta.atrStop(${CLOSE}, ${pick(args, 0, ["atr", "atr_value"], "null")}, ${finiteOr(pick(args, 1, ["multiplier", "mult"], "null"), "2")})`;
1787
+ }
1788
+ if (method === "zigzag") {
1789
+ const call = taCall("zigzag", site, [
1790
+ pick(args, 0, ["source", "series"], "null"),
1791
+ finiteOr(pick(args, 1, ["threshold", "percent", "deviation"], "null"), "5"),
1792
+ ]);
1793
+ return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
1404
1794
  }
1405
1795
  return "null";
1406
1796
  }