@hoox-sh/pynets 0.3.0 → 0.5.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.
@@ -46,6 +46,8 @@ const NS = new Set([
46
46
  "syminfo",
47
47
  "timenow",
48
48
  "chart",
49
+ "footprint",
50
+ "volume_row",
49
51
  ]);
50
52
 
51
53
  /** TaEngine methods we emit. Unknown `ta.*` → `null`. */
@@ -136,6 +138,23 @@ const TA_METHODS = new Set([
136
138
  "fractal",
137
139
  "atr_stop",
138
140
  "zigzag",
141
+ "hammer",
142
+ "gap_detector",
143
+ "macd_signal",
144
+ "acceleration_factor",
145
+ "stoch_smooth",
146
+ "engulfing",
147
+ "inside_bar_pattern",
148
+ "double_top_bottom",
149
+ "voi",
150
+ "bid_ask_imbalance",
151
+ "rsi_oversold_overbought",
152
+ "rsi_divergence",
153
+ "cumulative_delta",
154
+ "volume_momentum",
155
+ "volume_profile_high",
156
+ "volume_profile_low",
157
+ "volume_thrust",
139
158
  ]);
140
159
 
141
160
  /**
@@ -367,7 +386,15 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
367
386
  return `(() => { throw new Error(String(${msg})); })()`;
368
387
  }
369
388
 
370
- if (name === "alertcondition") return "null";
389
+ if (name === "max_bars_back") return "null";
390
+ if (name === "timenow") return "__h.timenow(time_arr)";
391
+ if (name === "alertcondition") {
392
+ ctx.usesDrawings = true;
393
+ const cond = pick(args, 0, ["condition"], "null");
394
+ const title = hasArg(args, 1, ["title"]) ? pick(args, 1, ["title"], `"Alert"`) : `"Alert"`;
395
+ const message = hasArg(args, 2, ["message"]) ? pick(args, 2, ["message"], `"Alert"`) : `"Alert"`;
396
+ return `__h.draw.alertcondition(__bar_idx, ${cond}, ${title}, ${message})`;
397
+ }
371
398
  if (name === "str_tostring" || name === "tostring") {
372
399
  const x = pick(args, 0, ["value", "source", "x"], "null");
373
400
  return `String(${x} ?? "")`;
@@ -428,6 +455,8 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
428
455
 
429
456
  const req = emitRequest(ctx, name, args);
430
457
  if (req != null) return req;
458
+ const fp = emitFootprint(name, args);
459
+ if (fp != null) return fp;
431
460
 
432
461
  return "null";
433
462
  }
@@ -525,6 +554,19 @@ function finiteOr(expr: string, fb: string): string {
525
554
  return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
526
555
  }
527
556
 
557
+ /** Interpret `unwrap`: bool → 1/0, non-finite → null. */
558
+ function cellOf(expr: string): string {
559
+ return `((__c) => (__c == null ? null : typeof __c === "boolean" ? (__c ? 1 : 0) : (typeof __c === "number" && Number.isFinite(__c) ? __c : null)))(${expr})`;
560
+ }
561
+
562
+ /**
563
+ * Interpret `wave10bList`: a Pine array becomes its cells; a scalar is a
564
+ * one-element list. Bar-mode series stay one element.
565
+ */
566
+ function cellList(expr: string): string {
567
+ 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)]; })()`;
568
+ }
569
+
528
570
  const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
529
571
 
530
572
  const ARRAY_SIGS: Record<string, string[][]> = {
@@ -704,6 +746,10 @@ function emitHostCall(ns: string, method: string, args: CallArgs, params: string
704
746
  function emitArray(name: string, args: CallArgs): string | null {
705
747
  const raw = nsMethod(name, "array_");
706
748
  if (raw == null) return null;
749
+ if (raw === "from") {
750
+ const parts = args.pos.length > 0 ? args.pos : Object.values(args.kw);
751
+ return `__h.array.from([${parts.join(", ")}])`;
752
+ }
707
753
  const method = ctorMethod(raw);
708
754
  const params = ARRAY_SIGS[method];
709
755
  if (params == null) return null;
@@ -745,11 +791,24 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
745
791
  label: {
746
792
  set_text: [["id"], ["text"]],
747
793
  set_xy: [["id"], ["x"], ["y"]],
794
+ set_x: [["id"], ["x"]],
795
+ set_y: [["id"], ["y"]],
748
796
  set_color: [["id"], ["color"]],
749
797
  set_style: [["id"], ["style"]],
750
798
  set_size: [["id"], ["size"]],
751
799
  set_tooltip: [["id"], ["tooltip"]],
752
800
  set_textalign: [["id"], ["textalign"]],
801
+ set_textcolor: [["id"], ["color", "textcolor"]],
802
+ set_text_font_family: [["id"], ["font_family", "text_font_family"]],
803
+ set_text_halign: [["id"], ["halign", "text_halign"]],
804
+ set_text_valign: [["id"], ["valign", "text_valign"]],
805
+ set_text_size: [["id"], ["size", "text_size"]],
806
+ set_text_formatting: [["id"], ["formatting", "text_formatting"]],
807
+ set_border_color: [["id"], ["color"]],
808
+ set_border_width: [["id"], ["width"]],
809
+ set_border_style: [["id"], ["style"]],
810
+ set_xloc: [["id"], ["xloc"]],
811
+ set_yloc: [["id"], ["yloc"]],
753
812
  delete: [["id"]],
754
813
  get_text: [["id"]],
755
814
  get_x: [["id"]],
@@ -757,12 +816,17 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
757
816
  },
758
817
  line: {
759
818
  set_xy: [["id"], ["x1"], ["y1"], ["x2"], ["y2"]],
760
- set_xy1: [["id"], ["x1"], ["y1"]],
761
- set_xy2: [["id"], ["x2"], ["y2"]],
819
+ set_xy1: [["id"], ["x1", "x"], ["y1", "y"]],
820
+ set_xy2: [["id"], ["x2", "x"], ["y2", "y"]],
821
+ set_x1: [["id"], ["x1", "x"]],
822
+ set_y1: [["id"], ["y1", "y"]],
823
+ set_x2: [["id"], ["x2", "x"]],
824
+ set_y2: [["id"], ["y2", "y"]],
762
825
  set_color: [["id"], ["color"]],
763
826
  set_width: [["id"], ["width"]],
764
827
  set_style: [["id"], ["style"]],
765
828
  set_extend: [["id"], ["extend"]],
829
+ set_xloc: [["id"], ["xloc"]],
766
830
  delete: [["id"]],
767
831
  get_price: [["id"], ["x"]],
768
832
  get_x1: [["id"]],
@@ -773,44 +837,144 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
773
837
  box: {
774
838
  set_corners: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
775
839
  set_lefttop: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
840
+ set_left: [["id"], ["left"]],
841
+ set_right: [["id"], ["right"]],
842
+ set_top: [["id"], ["top"]],
843
+ set_bottom: [["id"], ["bottom"]],
776
844
  set_bgcolor: [["id"], ["bgcolor", "color"]],
777
845
  set_border_color: [["id"], ["color", "border_color"]],
846
+ set_border_width: [["id"], ["width"]],
847
+ set_border_style: [["id"], ["style"]],
778
848
  set_text: [["id"], ["text"]],
849
+ set_text_color: [["id"], ["color", "text_color"]],
850
+ set_text_font_family: [["id"], ["font_family"]],
851
+ set_text_halign: [["id"], ["halign"]],
852
+ set_text_valign: [["id"], ["valign"]],
853
+ set_text_size: [["id"], ["size"]],
854
+ set_text_formatting: [["id"], ["formatting"]],
855
+ set_text_wrap: [["id"], ["wrap"]],
779
856
  set_extend: [["id"], ["extend"]],
857
+ set_closed: [["id"], ["closed"]],
858
+ set_rightbottom: [["id"], ["right"], ["bottom"]],
859
+ get_left: [["id"]],
860
+ get_right: [["id"]],
861
+ get_top: [["id"]],
862
+ get_bottom: [["id"]],
780
863
  delete: [["id"]],
781
864
  },
782
865
  table: {
783
866
  cell: [["id"], ["column", "col"], ["row"], ["text"]],
784
- cell_set_text: [["id"], ["column", "col"], ["row"], ["text"]],
867
+ // Interpret positional order is (id, row, column, text), not column-first.
868
+ cell_set_text: [["id"], ["row"], ["column", "col"], ["text"]],
869
+ cell_get_text: [["id"], ["row"], ["column", "col"]],
870
+ cell_set_text_color: [["id"], ["row"], ["column", "col"], ["color", "text"]],
871
+ cell_set_bgcolor: [["id"], ["row"], ["column", "col"], ["color"]],
872
+ cell_set_border_color: [["id"], ["row"], ["column", "col"], ["color"]],
873
+ cell_set_border_width: [["id"], ["row"], ["column", "col"], ["width"]],
874
+ cell_set_tooltip: [["id"], ["row"], ["column", "col"], ["tooltip", "text"]],
875
+ cell_set_width: [["id"], ["column", "col"], ["row"], ["width"]],
876
+ cell_set_height: [["id"], ["column", "col"], ["row"], ["height"]],
877
+ cell_set_text_halign: [["id"], ["column", "col"], ["row"], ["halign"]],
878
+ cell_set_text_valign: [["id"], ["column", "col"], ["row"], ["valign"]],
879
+ cell_set_text_size: [["id"], ["column", "col"], ["row"], ["size"]],
880
+ cell_set_text_font_family: [["id"], ["column", "col"], ["row"], ["font_family"]],
881
+ cell_set_text_formatting: [["id"], ["column", "col"], ["row"], ["formatting"]],
785
882
  set_position: [["id"], ["position"]],
883
+ set_bgcolor: [["id"], ["color", "bgcolor"]],
884
+ set_border_color: [["id"], ["color"]],
885
+ set_border_width: [["id"], ["width"]],
886
+ set_frame_color: [["id"], ["color"]],
887
+ set_frame_width: [["id"], ["width"]],
888
+ merge_cells: [["id"], ["start_row"], ["start_column", "start_col"], ["end_row"], ["end_column", "end_col"]],
786
889
  delete: [["id"]],
787
890
  },
788
891
  polyline: {
789
892
  delete: [["id"]],
893
+ get_points: [["id"]],
894
+ set_points: [["id"], ["points"]],
895
+ set_line_color: [["id"], ["color"]],
896
+ set_line_width: [["id"], ["width"]],
897
+ set_line_style: [["id"], ["style"]],
898
+ set_fill_color: [["id"], ["fill_color", "color"]],
899
+ set_curved: [["id"], ["curved"]],
900
+ set_force_overlay: [["id"], ["force_overlay"]],
901
+ set_closed: [["id"], ["closed"]],
902
+ set_xloc: [["id"], ["xloc"]],
790
903
  },
791
904
  linefill: {
792
905
  set_color: [["id"], ["color"]],
793
906
  delete: [["id"]],
907
+ get_line1: [["id"]],
908
+ get_line2: [["id"]],
794
909
  },
795
910
  };
796
911
 
912
+ function emitChartPoint(name: string, args: CallArgs): string | null {
913
+ const meth = name.slice("chart_point_".length);
914
+ if (meth === "new" || meth === "from_time") {
915
+ return `__h.draw.point_new(${pick(args, 0, ["time"], "null")}, ${pick(args, 1, ["price"], "0")})`;
916
+ }
917
+ if (meth === "from_index") {
918
+ return `__h.draw.point_from_index(${pick(args, 0, ["index"], "null")}, ${pick(args, 1, ["price"], "0")})`;
919
+ }
920
+ if (meth === "now") {
921
+ return `__h.draw.point_now(__bar_idx, time, ${pick(args, 0, ["price"], "0")})`;
922
+ }
923
+ if (meth === "copy") {
924
+ return `__h.draw.point_copy(${pick(args, 0, ["point"], "null")})`;
925
+ }
926
+ return null;
927
+ }
928
+
797
929
  function emitDraw(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
798
930
  if (name === "alert" && methodSrc == null) {
799
931
  ctx.usesDrawings = true;
800
932
  return `__h.draw.alert(__bar_idx, ${pick(args, 0, ["message"], "null")})`;
801
933
  }
934
+ if (name.startsWith("chart_point_")) {
935
+ const point = emitChartPoint(name, args);
936
+ if (point != null) ctx.usesDrawings = true;
937
+ return point;
938
+ }
802
939
  const parsed = drawKindAndMethod(name, methodSrc);
803
940
  if (parsed == null) return null;
804
941
  const { kind, method } = parsed;
805
942
  let code: string | null = null;
806
- if (method === "new") {
943
+ if (method === "all") {
944
+ code = `__h.draw.all(${JSON.stringify(kind)})`;
945
+ } else if (method === "copy" && (kind === "line" || kind === "label" || kind === "box" || kind === "polyline")) {
946
+ code = `__h.draw.copy(${pick(args, 0, ["id"], "null")})`;
947
+ } else if (method === "new") {
807
948
  if (kind === "label") {
808
- code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")})`;
949
+ code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")}, ${pick(args, 0, ["x"], "0")}, ${pick(args, 1, ["y"], "0")})`;
950
+ } else if (kind === "line") {
951
+ 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")})`;
952
+ } else if (kind === "box") {
953
+ 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")})`;
954
+ } else if (kind === "polyline") {
955
+ code = `__h.draw.polyline_new(__bar_idx, ${pick(args, 0, ["points"], "null")})`;
809
956
  } else if (kind === "linefill") {
810
- code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1"], "null")}, ${pick(args, 1, ["id2"], "null")})`;
957
+ const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
958
+ code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
811
959
  } else if (DRAW_KINDS.has(kind)) {
812
960
  code = `__h.draw.${kind}_new(__bar_idx)`;
813
961
  }
962
+ } else if (kind === "line" && method === "fill") {
963
+ const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
964
+ code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
965
+ } else if (
966
+ (kind === "line" && (method === "set_first_point" || method === "set_second_point")) ||
967
+ (kind === "label" && method === "set_point") ||
968
+ (kind === "box" && (method === "set_top_left_point" || method === "set_bottom_right_point"))
969
+ ) {
970
+ code = `__h.draw.${kind}_${method}(${pick(args, 0, ["id"], "null")}, ${pick(args, 1, ["point"], "null")})`;
971
+ } else if (kind === "box" && method === "set_xloc") {
972
+ 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")})`;
973
+ } else if (kind === "table" && method === "clear") {
974
+ const id = pick(args, 0, ["id", "table_id"], "null");
975
+ code = hasArg(args, 1, ["start_row"])
976
+ ? `__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")})`
977
+ : `__h.draw.table_clear(${id})`;
814
978
  } else {
815
979
  const aliased = kind === "box" && method === "set_lefttop" ? "set_corners" : method;
816
980
  const params = DRAW_METHODS[kind]?.[method] ?? DRAW_METHODS[kind]?.[aliased];
@@ -855,9 +1019,9 @@ function emitUdt(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallA
855
1019
 
856
1020
  const SECURITY_FUNCS = new Set([
857
1021
  "security",
1022
+ "security_lower_tf",
858
1023
  "request_security",
859
1024
  "request_security_lower_tf",
860
- "request_seed",
861
1025
  ]);
862
1026
 
863
1027
  const SIMPLE_CHART_EXPR = new Set([
@@ -903,10 +1067,60 @@ function emitRequest(ctx: EmitCtx, name: string, args: CallArgs): string | null
903
1067
  if (name === "request_currency_rate" || name === "currency_rate") {
904
1068
  return `__h.currencyRate(${pick(args, 0, ["from", "from_currency"], "null")}, ${pick(args, 1, ["to", "to_currency"], "null")})`;
905
1069
  }
1070
+ if (name === "request_seed") {
1071
+ return `__h.requestSeed(${pick(args, 0, ["seed"], "0")})`;
1072
+ }
1073
+ if (name === "request_footprint") {
1074
+ return `__h.requestFootprint(${pick(args, 0, ["num_ticks"], "100")}, ${pick(args, 1, ["va_percentage"], "70")})`;
1075
+ }
906
1076
  if (name.startsWith("request_")) return "null";
907
1077
  return null;
908
1078
  }
909
1079
 
1080
+ /** Python `request.footprint` mock + `footprint.*` / `volume_row.*` accessors. */
1081
+ function emitFootprint(name: string, args: CallArgs): string | null {
1082
+ const fp = pick(args, 0, ["id", "footprint"], "null");
1083
+ const vr = pick(args, 0, ["id", "volume_row"], "null");
1084
+ switch (name) {
1085
+ case "footprint_buy_volume":
1086
+ return `__h.footprint.buy_volume(${fp})`;
1087
+ case "footprint_sell_volume":
1088
+ return `__h.footprint.sell_volume(${fp})`;
1089
+ case "footprint_delta":
1090
+ return `__h.footprint.delta(${fp})`;
1091
+ case "footprint_total_volume":
1092
+ return `__h.footprint.total_volume(${fp})`;
1093
+ case "footprint_vah":
1094
+ return `__h.footprint.vah(${fp})`;
1095
+ case "footprint_val":
1096
+ return `__h.footprint.val(${fp})`;
1097
+ case "footprint_poc":
1098
+ return `__h.footprint.poc(${fp})`;
1099
+ case "footprint_rows":
1100
+ return `__h.footprint.rows(${fp})`;
1101
+ case "footprint_get_row_by_price":
1102
+ return `__h.footprint.get_row_by_price(${fp}, ${pick(args, 1, ["price"], "null")})`;
1103
+ case "volume_row_up_price":
1104
+ return `__h.footprint.volume_row.up_price(${vr})`;
1105
+ case "volume_row_down_price":
1106
+ return `__h.footprint.volume_row.down_price(${vr})`;
1107
+ case "volume_row_buy_volume":
1108
+ return `__h.footprint.volume_row.buy_volume(${vr})`;
1109
+ case "volume_row_sell_volume":
1110
+ return `__h.footprint.volume_row.sell_volume(${vr})`;
1111
+ case "volume_row_delta":
1112
+ return `__h.footprint.volume_row.delta(${vr})`;
1113
+ case "volume_row_total_volume":
1114
+ return `__h.footprint.volume_row.total_volume(${vr})`;
1115
+ case "volume_row_has_buy_imbalance":
1116
+ return `__h.footprint.volume_row.has_buy_imbalance(${vr})`;
1117
+ case "volume_row_has_sell_imbalance":
1118
+ return `__h.footprint.volume_row.has_sell_imbalance(${vr})`;
1119
+ default:
1120
+ return null;
1121
+ }
1122
+ }
1123
+
910
1124
  function isSimpleSecurityExpr(expr: string): boolean {
911
1125
  const e = expr.trim();
912
1126
  if (!e) return false;
@@ -1024,6 +1238,7 @@ function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
1024
1238
  add(["default_qty_type"], "default_qty_type");
1025
1239
  add(["default_qty_value"], "default_qty_value");
1026
1240
  add(["initial_capital"], "initial_capital");
1241
+ add(["currency"], "currency");
1027
1242
  return `__h.strategy.configure({${fields.join(", ")}})`;
1028
1243
  }
1029
1244
 
@@ -1095,8 +1310,50 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
1095
1310
  case "margin_liquidation_price":
1096
1311
  case "position_avg_price":
1097
1312
  case "initial_capital":
1313
+ case "cash":
1314
+ case "position_entry_name":
1315
+ case "account_currency":
1316
+ case "max_contracts_held_all":
1317
+ case "max_contracts_held_long":
1318
+ case "max_contracts_held_short":
1319
+ case "wintrades":
1320
+ case "losstrades":
1321
+ case "eventrades":
1322
+ case "grossprofit":
1323
+ case "grossloss":
1324
+ case "avg_trade":
1325
+ case "avg_winning_trade":
1326
+ case "avg_losing_trade":
1327
+ case "netprofit_percent":
1328
+ case "openprofit_percent":
1329
+ case "grossprofit_percent":
1330
+ case "grossloss_percent":
1331
+ case "avg_trade_percent":
1332
+ case "avg_winning_trade_percent":
1333
+ case "avg_losing_trade_percent":
1334
+ case "max_drawdown":
1335
+ case "max_drawdown_percent":
1336
+ case "max_runup":
1337
+ case "max_runup_percent":
1338
+ case "percent_profitable":
1339
+ case "winrate":
1340
+ case "profitfactor":
1341
+ case "profit_factor":
1342
+ case "capital_held":
1343
+ case "closedtrades_first_index":
1098
1344
  ctx.usesStrategy = true;
1099
1345
  return `__h.strategy.${method}()`;
1346
+ case "commission":
1347
+ return "0";
1348
+ case "default_entry_qty":
1349
+ ctx.usesStrategy = true;
1350
+ return hasArg(args, 0, ["percent_equity"])
1351
+ ? `__h.strategy.default_entry_qty(${pick(args, 0, ["percent_equity"], "null")})`
1352
+ : `__h.strategy.default_entry_qty()`;
1353
+ case "convert_to_account":
1354
+ case "convert_to_symbol":
1355
+ ctx.usesStrategy = true;
1356
+ return hasArg(args, 0, ["value"]) ? `__h.strategy.convert_to(${pick(args, 0, ["value"], "null")})` : "1";
1100
1357
  case "risk_allow_entry_in":
1101
1358
  ctx.usesStrategy = true;
1102
1359
  return `__h.strategy.risk_allow_entry_in(${pick(args, 0, ["value"], "null")})`;
@@ -1106,9 +1363,33 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
1106
1363
  case "risk_max_drawdown":
1107
1364
  ctx.usesStrategy = true;
1108
1365
  return `__h.strategy.risk_max_drawdown(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["type"], "null")})`;
1109
- default:
1366
+ default: {
1367
+ if (method.startsWith("direction_")) {
1368
+ const tail = method.slice("direction_".length);
1369
+ if (tail === "long" || tail === "short" || tail === "all") return JSON.stringify(tail);
1370
+ }
1371
+ if (method.startsWith("oca_")) {
1372
+ const tail = method.slice("oca_".length);
1373
+ if (tail === "none" || tail === "cancel" || tail === "reduce") return JSON.stringify(tail);
1374
+ }
1375
+ if (method.startsWith("commission_")) {
1376
+ const tail = method.slice("commission_".length);
1377
+ if (tail === "percent" || tail === "cash_per_order" || tail === "cash_per_contract") {
1378
+ return JSON.stringify(tail);
1379
+ }
1380
+ }
1381
+ if (method.startsWith("closedtrades_") || method.startsWith("opentrades_")) {
1382
+ ctx.usesStrategy = true;
1383
+ if (method === "opentrades_capital_held") return "__h.strategy.capital_held()";
1384
+ if (method === "closedtrades_first_index") return "__h.strategy.closedtrades_first_index()";
1385
+ const idx = hasArg(args, 0, ["trade_num", "trade_index", "index"])
1386
+ ? pick(args, 0, ["trade_num", "trade_index", "index"], "0")
1387
+ : "0";
1388
+ return `__h.strategy.${method}(${idx})`;
1389
+ }
1110
1390
  if (isStrategyAction(name, method)) ctx.usesStrategy = true;
1111
1391
  return "null";
1392
+ }
1112
1393
  }
1113
1394
  }
1114
1395
 
@@ -1167,7 +1448,7 @@ function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
1167
1448
  : pick(args, 0, ["series", "source", "price"], "null");
1168
1449
  const i = ctx.plots.length;
1169
1450
  ctx.plots.push({ title });
1170
- return `plot_${i}[__bar_idx] = __h.naNum(${series})`;
1451
+ return `plot_${i}[__bar_idx] = __h.plotNum(${series})`;
1171
1452
  }
1172
1453
 
1173
1454
  function emitColor(name: string, args: CallArgs): string | null {
@@ -1792,5 +2073,80 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
1792
2073
  ]);
1793
2074
  return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
1794
2075
  }
2076
+ if (method === "hammer") {
2077
+ 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"))})`;
2078
+ }
2079
+ if (method === "gap_detector") {
2080
+ return `__h.ta.gapDetector(${cellOf(pick(args, 0, ["high"], "null"))}, ${cellOf(pick(args, 1, ["low"], "null"))}, ${cellOf(pick(args, 2, ["prev_close", "prev"], "null"))})`;
2081
+ }
2082
+ if (method === "macd_signal") {
2083
+ return `__h.ta.macdSignal(${cellOf(pick(args, 0, ["macd_line", "macd"], "null"))}, ${cellOf(pick(args, 1, ["signal_line", "signal"], "null"))})`;
2084
+ }
2085
+ if (method === "acceleration_factor") {
2086
+ return taCall("accelerationFactor", site, [
2087
+ cellOf(pick(args, 0, ["momentum_list", "momentum", "source", "series"], "null")),
2088
+ cellOf(pick(args, 1, ["period", "length"], "null")),
2089
+ ]);
2090
+ }
2091
+ if (method === "stoch_smooth") {
2092
+ 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"))})`;
2093
+ }
2094
+ if (method === "engulfing") {
2095
+ // Bar-mode sentinel. Args are still evaluated (interpret reads all four).
2096
+ 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(); })()`;
2097
+ }
2098
+ if (method === "inside_bar_pattern") {
2099
+ return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.insideBarPattern(); })()`;
2100
+ }
2101
+ if (method === "double_top_bottom") {
2102
+ return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.doubleTopBottom(${cellOf(pick(args, 2, ["period", "length"], "null"))}); })()`;
2103
+ }
2104
+ if (method === "voi") {
2105
+ return `__h.ta.voi(${cellOf(pick(args, 0, ["buy_volume", "buy"], "null"))}, ${cellOf(pick(args, 1, ["sell_volume", "sell"], "null"))})`;
2106
+ }
2107
+ if (method === "bid_ask_imbalance") {
2108
+ 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"))})`;
2109
+ }
2110
+ if (method === "rsi_oversold_overbought") {
2111
+ 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"))})`;
2112
+ }
2113
+ if (method === "rsi_divergence") {
2114
+ // Missing period is 0 (interpret `period ?? 0`), not the engine's null → na.
2115
+ const period = `((__n) => (__n == null ? 0 : __n))(${cellOf(pick(args, 1, ["period", "length"], "null"))})`;
2116
+ return `__h.ta.rsiDivergence(${cellList(pick(args, 0, ["rsi_series", "series", "source"], "null"))}, ${period})`;
2117
+ }
2118
+ if (method === "cumulative_delta") {
2119
+ return taCall("cumulativeDelta", site, [
2120
+ cellOf(pick(args, 0, ["close", "source"], "null")),
2121
+ cellOf(pick(args, 1, ["volume"], "null")),
2122
+ cellOf(pick(args, 2, ["period", "length"], "null")),
2123
+ ]);
2124
+ }
2125
+ if (method === "volume_momentum") {
2126
+ return taCall("volumeMomentum", site, [
2127
+ cellOf(pick(args, 0, ["volume", "source"], "null")),
2128
+ cellOf(pick(args, 1, ["period", "length"], "null")),
2129
+ ]);
2130
+ }
2131
+ if (method === "volume_profile_high") {
2132
+ return `__h.ta.volumeProfileHigh(${cellList(pick(args, 0, ["close", "price", "source", "series"], "null"))}, ${cellList(pick(args, 1, ["volume"], "null"))})`;
2133
+ }
2134
+ if (method === "volume_profile_low") {
2135
+ return taCall("volumeProfileLow", site, [
2136
+ cellOf(pick(args, 0, ["close", "source"], "null")),
2137
+ cellOf(pick(args, 1, ["volume"], "null")),
2138
+ cellOf(pick(args, 2, ["period", "length"], "null")),
2139
+ cellOf(pick(args, 3, ["levels"], "null")),
2140
+ ]);
2141
+ }
2142
+ if (method === "volume_thrust") {
2143
+ const sens = cellOf(pick(args, 3, ["sensitivity"], "null"));
2144
+ return taCall("volumeThrust", site, [
2145
+ cellOf(pick(args, 0, ["close", "source"], "null")),
2146
+ cellOf(pick(args, 1, ["volume"], "null")),
2147
+ cellOf(pick(args, 2, ["volume_sma", "vol_sma", "sma"], "null")),
2148
+ `((__n) => (__n == null ? 0.3 : __n))(${sens})`,
2149
+ ]);
2150
+ }
1795
2151
  return "null";
1796
2152
  }
@@ -56,7 +56,28 @@ import {
56
56
  strUpper,
57
57
  } from "../str.ts";
58
58
  import { LogBook, formatLogParts } from "../log.ts";
59
- import { resolveCurrencyRate } from "../request.ts";
59
+ import { PineArray } from "../array.ts";
60
+ import {
61
+ createFootprint,
62
+ footprintBuyVolume,
63
+ footprintDelta,
64
+ footprintGetRowByPrice,
65
+ footprintPoc,
66
+ footprintRows,
67
+ footprintSellVolume,
68
+ footprintTotalVolume,
69
+ footprintVah,
70
+ footprintVal,
71
+ volumeRowBuyVolume,
72
+ volumeRowDelta,
73
+ volumeRowDownPrice,
74
+ volumeRowHasBuyImbalance,
75
+ volumeRowHasSellImbalance,
76
+ volumeRowSellVolume,
77
+ volumeRowTotalVolume,
78
+ volumeRowUpPrice,
79
+ } from "../footprint.ts";
80
+ import { resolveCurrencyRate, resolveSeed } from "../request.ts";
60
81
  import { TaEngine } from "../ta.ts";
61
82
  import { utcPartsFromMs, timestamp, weekOfYear, timeTradingDay, type UtcParts } from "../time.ts";
62
83
  import { TickerId } from "../ticker.ts";
@@ -83,6 +104,7 @@ import {
83
104
  safeNeg,
84
105
  safeSub,
85
106
  } from "./helpers.ts";
107
+ import { isStrategyCashAmount } from "../strategy.ts";
86
108
  import { createCompileStrategy } from "./strategy.ts";
87
109
  import { udtGet, udtNew, udtRegister, udtSet } from "./udt_runtime.ts";
88
110
 
@@ -105,6 +127,8 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
105
127
  na: NA_FN,
106
128
  nz,
107
129
  naNum,
130
+ plotNum,
131
+ timenow,
108
132
  isNa: isNaCell,
109
133
  eq: pineEq,
110
134
  ne: pineNe,
@@ -224,12 +248,41 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
224
248
  udtSet,
225
249
  udtRegister,
226
250
  currencyRate: resolveCurrencyRate,
251
+ requestSeed: resolveSeed,
252
+ requestFootprint: createFootprint,
253
+ footprint: {
254
+ buy_volume: footprintBuyVolume,
255
+ sell_volume: footprintSellVolume,
256
+ delta: footprintDelta,
257
+ total_volume: footprintTotalVolume,
258
+ vah: footprintVah,
259
+ val: footprintVal,
260
+ poc: footprintPoc,
261
+ rows: compileFootprintRows,
262
+ get_row_by_price: footprintGetRowByPrice,
263
+ volume_row: {
264
+ up_price: volumeRowUpPrice,
265
+ down_price: volumeRowDownPrice,
266
+ buy_volume: volumeRowBuyVolume,
267
+ sell_volume: volumeRowSellVolume,
268
+ delta: volumeRowDelta,
269
+ total_volume: volumeRowTotalVolume,
270
+ has_buy_imbalance: volumeRowHasBuyImbalance,
271
+ has_sell_imbalance: volumeRowHasSellImbalance,
272
+ },
273
+ },
227
274
  runtimeError(msg: unknown): never {
228
275
  throw new Error(String(msg));
229
276
  },
230
277
  };
231
278
  }
232
279
 
280
+ function compileFootprintRows(fp: unknown): PineArray {
281
+ const arr = new PineArray();
282
+ for (const row of footprintRows(fp)) arr.push(row);
283
+ return arr;
284
+ }
285
+
233
286
  /** Pine `color.new(color, transp)` — first arg is a color, not r,g,b. */
234
287
  function compileColorNew(color: unknown, transp?: unknown) {
235
288
  const parsed = asColor(color);
@@ -264,6 +317,23 @@ function iff(cond: unknown, thenV: unknown, elseV: unknown): unknown {
264
317
  return mathIff(cond as number | null, thenV as number | null, elseV as number | null);
265
318
  }
266
319
 
320
+ /** Plot cell: tagged `strategy.cash` unwraps to its number (interpret `unwrap`). */
321
+ function plotNum(v: unknown): number | null {
322
+ if (isStrategyCashAmount(v)) return Number.isFinite(v.value) ? v.value : null;
323
+ return naNum(v);
324
+ }
325
+
326
+ /**
327
+ * Dataset last-bar time on every bar. Interpret `beginBar` stores that on
328
+ * `ctx.timenow` (host seed is overwritten). Not wall-clock, not current bar.
329
+ */
330
+ function timenow(timeArr: unknown): number {
331
+ if (!Array.isArray(timeArr) || timeArr.length === 0) return 0;
332
+ const last = timeArr[timeArr.length - 1];
333
+ if (typeof last === "number" && Number.isFinite(last)) return last;
334
+ return (timeArr.length - 1) * 60_000;
335
+ }
336
+
267
337
  function NA_FN(v?: unknown): number | null {
268
338
  if (arguments.length === 0) return null;
269
339
  return isNaCell(v) ? 1 : 0;