@hoox-sh/pynets 0.4.0 → 0.6.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.
@@ -114,7 +114,7 @@ import {
114
114
  mathToDegrees,
115
115
  mathToRadians,
116
116
  } from "./math.ts";
117
- import { utcPartsFromMs, timestamp, timeTradingDay, weekOfYear } from "./time.ts";
117
+ import { utcPartsFromMs, timestamp, timestampToMs, timeTradingDay, weekOfYear } from "./time.ts";
118
118
  import {
119
119
  LibraryModule,
120
120
  LibraryRegistry,
@@ -123,6 +123,27 @@ import {
123
123
  type StubKwargs,
124
124
  } from "./library.ts";
125
125
  import { isRequestBuiltin, resolveRequest, resolveSecurity } from "./request.ts";
126
+ import {
127
+ Footprint,
128
+ footprintBuyVolume,
129
+ footprintDelta,
130
+ footprintGetRowByPrice,
131
+ footprintPoc,
132
+ footprintRows,
133
+ footprintSellVolume,
134
+ footprintTotalVolume,
135
+ footprintVah,
136
+ footprintVal,
137
+ volumeRowBuyVolume,
138
+ volumeRowDelta,
139
+ volumeRowDownPrice,
140
+ volumeRowHasBuyImbalance,
141
+ volumeRowHasSellImbalance,
142
+ volumeRowSellVolume,
143
+ volumeRowTotalVolume,
144
+ volumeRowUpPrice,
145
+ type VolumeRow,
146
+ } from "./footprint.ts";
126
147
  import { NA, PineSeries, type Cell } from "./series.ts";
127
148
  import {
128
149
  strContains,
@@ -254,6 +275,8 @@ type Value =
254
275
  | PineMap
255
276
  | PineMatrix
256
277
  | TickerId
278
+ | Footprint
279
+ | VolumeRow
257
280
  | UdtType
258
281
  | UdtInstance
259
282
  | LibraryModule
@@ -337,7 +360,7 @@ interface Env {
337
360
  onceFired: WeakSet<object>;
338
361
  /** Python `_max_bars_back_decls` — recorded only; evaluation is unbounded. */
339
362
  maxBarsBackDecls: Array<{ var: Value; num: Value }>;
340
- /** Python `_alert_fire_bars` keys for `alertcondition` once_per_bar. */
363
+ /** Python `_alert_fire_bars` keys for `alert` / `alertcondition` freq gating. */
341
364
  alertFireBars: Map<string, number>;
342
365
  }
343
366
 
@@ -565,6 +588,7 @@ export function interpretTree(
565
588
  high: num(env.ctx.high) ?? undefined,
566
589
  low: num(env.ctx.low) ?? undefined,
567
590
  close: num(env.ctx.close) ?? undefined,
591
+ time: num(env.ctx.time) ?? undefined,
568
592
  });
569
593
  env.book.markOpenTrades(
570
594
  num(env.ctx.high) ?? num(env.ctx.close) ?? 0,
@@ -914,6 +938,35 @@ function seriesGet(env: Env, id: string): PineSeries | undefined {
914
938
  return env.series.get(id);
915
939
  }
916
940
 
941
+ /** Bare series names Python's evaluator exposes as `PineSeries` objects. */
942
+ const BARE_SERIES_NAMES = new Set([
943
+ "open",
944
+ "high",
945
+ "low",
946
+ "close",
947
+ "volume",
948
+ "hl2",
949
+ "hlc3",
950
+ "ohlc4",
951
+ "hlcc4",
952
+ "time",
953
+ "time_close",
954
+ ]);
955
+
956
+ /**
957
+ * True when an argument expression is a series reference rather than a scalar.
958
+ * Python handlers that do `args[0] if isinstance(args[0], list) else [args[0]]`
959
+ * (e.g. `ta.parkinson`, `ta.garman_klass`) see the `PineSeries` object in bar
960
+ * mode and therefore return `na`.
961
+ */
962
+ function isSeriesExpr(value: expr, env: Env): boolean {
963
+ if (value.kind === "Name") {
964
+ if (BARE_SERIES_NAMES.has(value.id)) return true;
965
+ return seriesGet(env, value.id) != null;
966
+ }
967
+ return false;
968
+ }
969
+
917
970
  function bindName(env: Env, id: string, value: Value): void {
918
971
  const stored: Value = typeof value === "number" && !Number.isFinite(value) ? NA : value;
919
972
  const frame = activeFrame(env);
@@ -1510,6 +1563,22 @@ export const MATH_CONSTANTS: Record<string, Value> = {
1510
1563
  "plot.linestyle_solid": "linestyle_solid",
1511
1564
  "plot.linestyle_dashed": "linestyle_dashed",
1512
1565
  "plot.linestyle_dotted": "linestyle_dotted",
1566
+ // Python plotting.py PlotStyle tokens — longer than hline's "solid".
1567
+ "plot.style_line": "style_line",
1568
+ "plot.style_linebr": "style_linebr",
1569
+ "plot.style_stepline": "style_stepline",
1570
+ "plot.style_steplinebr": "style_steplinebr",
1571
+ "plot.style_stepline_diamond": "style_stepline_diamond",
1572
+ "plot.style_histogram": "style_histogram",
1573
+ "plot.style_columns": "style_columns",
1574
+ "plot.style_cross": "style_cross",
1575
+ "plot.style_area": "style_area",
1576
+ "plot.style_areabr": "style_areabr",
1577
+ "plot.style_circles": "style_circles",
1578
+ // Python alerts.py `_alerts_builtin_map`.
1579
+ "alert.freq_once_per_bar": "once_per_bar",
1580
+ "alert.freq_once_per_bar_close": "once_per_bar_close",
1581
+ "alert.freq_all": "all",
1513
1582
  "dayofweek.sunday": 1,
1514
1583
  "dayofweek.monday": 2,
1515
1584
  "dayofweek.tuesday": 3,
@@ -1889,6 +1958,8 @@ function evalCall(node: Call, env: Env): Value {
1889
1958
  ) {
1890
1959
  return evalRequestCall(fname!, node, env);
1891
1960
  }
1961
+ const fpCall = evalFootprintCall(fname, node, env);
1962
+ if (fpCall !== undefined) return fpCall;
1892
1963
  const arrVal = evalArrayCall(fname, node, env);
1893
1964
  if (arrVal !== undefined) return arrVal;
1894
1965
  const colorVal = evalColorCall(fname, node, env);
@@ -2066,6 +2137,7 @@ function evalCall(node: Call, env: Env): Value {
2066
2137
  time: num(env.ctx.time) ?? undefined,
2067
2138
  oca_name: evalAsString(callArg(node.args, -1, ["oca_name"]), env),
2068
2139
  oca_type: evalAsString(callArg(node.args, -1, ["oca_type"]), env),
2140
+ deferMarket: true,
2069
2141
  });
2070
2142
  return NA;
2071
2143
  }
@@ -2074,12 +2146,10 @@ function evalCall(node: Call, env: Env): Value {
2074
2146
  const mark = num(env.ctx.close);
2075
2147
  const qtyExpr = callArg(node.args, 1, ["qty"]);
2076
2148
  const qtyRaw = qtyExpr == null ? undefined : unwrap(evalExpr(qtyExpr, env));
2077
- if (env.book.position.qty !== 0 && mark != null) {
2078
- env.book.fillClose(env.barIndex, id, mark, {
2079
- time: num(env.ctx.time) ?? undefined,
2080
- qty: qtyRaw ?? undefined,
2081
- });
2082
- } else env.book.close(env.barIndex, id);
2149
+ env.book.placeClose(env.barIndex, id, mark ?? undefined, {
2150
+ time: num(env.ctx.time) ?? undefined,
2151
+ qty: qtyRaw ?? undefined,
2152
+ });
2083
2153
  return NA;
2084
2154
  }
2085
2155
  if (fname === "strategy.exit") {
@@ -2099,17 +2169,12 @@ function evalCall(node: Call, env: Env): Value {
2099
2169
  trail_offset: unwrap(evalExpr(callArg(node.args, 10, ["trail_offset"]), env)),
2100
2170
  price: num(env.ctx.close) ?? undefined,
2101
2171
  time: num(env.ctx.time) ?? undefined,
2102
- ohlc: {
2103
- open: num(env.ctx.open) ?? undefined,
2104
- high: num(env.ctx.high) ?? undefined,
2105
- low: num(env.ctx.low) ?? undefined,
2106
- close: num(env.ctx.close) ?? undefined,
2107
- },
2172
+ deferMarket: true,
2108
2173
  });
2109
2174
  return NA;
2110
2175
  }
2111
2176
  if (fname === "strategy.close_all") {
2112
- env.book.closeAll(env.barIndex, num(env.ctx.close) ?? undefined);
2177
+ env.book.placeCloseAll(env.barIndex, num(env.ctx.close) ?? undefined);
2113
2178
  return NA;
2114
2179
  }
2115
2180
  if (fname === "strategy.cancel") {
@@ -2132,6 +2197,7 @@ function evalCall(node: Call, env: Env): Value {
2132
2197
  time: num(env.ctx.time) ?? undefined,
2133
2198
  oca_name: evalAsString(callArg(node.args, -1, ["oca_name"]), env),
2134
2199
  oca_type: evalAsString(callArg(node.args, -1, ["oca_type"]), env),
2200
+ deferMarket: true,
2135
2201
  });
2136
2202
  return NA;
2137
2203
  }
@@ -2577,14 +2643,66 @@ function packDrawings(book: DrawingBook): DrawingEvent[] | undefined {
2577
2643
  return book.items.length === 0 ? undefined : book.items;
2578
2644
  }
2579
2645
 
2580
- /** Python `_emit_alert(..., source="alertcondition", freq=once_per_bar)`. */
2581
- function fireAlertCondition(env: Env, title: string, message: string): void {
2582
- const key = `alertcondition|${title}|${message}|once_per_bar`;
2583
- if (env.alertFireBars.get(key) === env.barIndex) return;
2646
+ const FREQ_ONCE_PER_BAR = "once_per_bar";
2647
+ const FREQ_ONCE_PER_BAR_CLOSE = "once_per_bar_close";
2648
+ const FREQ_ALL = "all";
2649
+
2650
+ const ALERT_FREQ_ALIASES: Record<string, string> = {
2651
+ once_per_bar: FREQ_ONCE_PER_BAR,
2652
+ freq_once_per_bar: FREQ_ONCE_PER_BAR,
2653
+ "alert.freq_once_per_bar": FREQ_ONCE_PER_BAR,
2654
+ once_per_bar_close: FREQ_ONCE_PER_BAR_CLOSE,
2655
+ freq_once_per_bar_close: FREQ_ONCE_PER_BAR_CLOSE,
2656
+ "alert.freq_once_per_bar_close": FREQ_ONCE_PER_BAR_CLOSE,
2657
+ all: FREQ_ALL,
2658
+ freq_all: FREQ_ALL,
2659
+ "alert.freq_all": FREQ_ALL,
2660
+ };
2661
+
2662
+ /** Python `normalize_alert_freq`. Unknown non-empty tokens are kept (still once-per-bar gated). */
2663
+ function normalizeAlertFreq(freq: unknown): string {
2664
+ if (freq == null) return FREQ_ONCE_PER_BAR;
2665
+ if (typeof freq === "number" && !Number.isFinite(freq)) return FREQ_ONCE_PER_BAR;
2666
+ const s = String(freq).trim().toLowerCase().replace(/ /g, "_");
2667
+ return ALERT_FREQ_ALIASES[s] ?? (s ? s : FREQ_ONCE_PER_BAR);
2668
+ }
2669
+
2670
+ /** Python `_bar_is_confirmed`. This host sets `barstate.isconfirmed` to 1 on every bar. */
2671
+ function barIsConfirmed(_env: Env): boolean {
2672
+ return true;
2673
+ }
2674
+
2675
+ /** Python `_should_fire_alert`. */
2676
+ function shouldFireAlert(env: Env, source: string, title: string, message: string, freq: string): boolean {
2677
+ const freqN = normalizeAlertFreq(freq);
2678
+ if (freqN === FREQ_ALL) return true;
2679
+ if (freqN === FREQ_ONCE_PER_BAR_CLOSE && !barIsConfirmed(env)) return false;
2680
+ const key = `${source}|${title}|${message}|${freqN}`;
2681
+ if (env.alertFireBars.get(key) === env.barIndex) return false;
2584
2682
  env.alertFireBars.set(key, env.barIndex);
2683
+ return true;
2684
+ }
2685
+
2686
+ /** Python `_emit_alert`. Records on the drawing book (no separate alerts array). */
2687
+ function emitAlert(
2688
+ env: Env,
2689
+ message: string,
2690
+ freq: unknown,
2691
+ opts?: { title?: string; source?: string },
2692
+ ): void {
2693
+ const source = opts?.source ?? "alert";
2694
+ const title = opts?.title ?? "";
2695
+ const freqN = normalizeAlertFreq(freq);
2696
+ if (!shouldFireAlert(env, source, title, message, freqN)) return;
2585
2697
  env.drawings.alert(env.barIndex, message);
2586
2698
  const ev = env.drawings.items[env.drawings.items.length - 1];
2587
- if (ev != null) ev.extra = { source: "alertcondition", title, freq: "once_per_bar" };
2699
+ if (ev == null) return;
2700
+ ev.extra = title ? { source, title, freq: freqN } : { source, freq: freqN };
2701
+ }
2702
+
2703
+ /** Python `_emit_alert(..., source="alertcondition", freq=once_per_bar)`. */
2704
+ function fireAlertCondition(env: Env, title: string, message: string): void {
2705
+ emitAlert(env, message, FREQ_ONCE_PER_BAR, { title, source: "alertcondition" });
2588
2706
  }
2589
2707
 
2590
2708
  function inputTitle(node: Call): string | null {
@@ -2895,7 +3013,7 @@ function evalRequestSecurity(node: Call, env: Env): Cell {
2895
3013
  );
2896
3014
  }
2897
3015
 
2898
- function evalRequestCall(fname: string, node: Call, env: Env): Cell {
3016
+ function evalRequestCall(fname: string, node: Call, env: Env): Value {
2899
3017
  if (
2900
3018
  fname === "request.security" ||
2901
3019
  fname === "request.security_lower_tf" ||
@@ -2916,10 +3034,75 @@ function evalRequestCall(fname: string, node: Call, env: Env): Cell {
2916
3034
  const v = resolveRequest(fname, host, { seed });
2917
3035
  return typeof v === "number" || v === null ? v : NA;
2918
3036
  }
3037
+ if (fname === "request.footprint") {
3038
+ const numTicks = unwrap(evalExpr(callArg(node.args, 0, ["num_ticks"]), env));
3039
+ const va = unwrap(evalExpr(callArg(node.args, 1, ["va_percentage"]), env));
3040
+ const v = resolveRequest(fname, host, {
3041
+ num_ticks: numTicks ?? 100,
3042
+ va_percentage: va ?? 70,
3043
+ });
3044
+ return v instanceof Footprint ? v : NA;
3045
+ }
2919
3046
  const v = resolveRequest(fname, host, {});
2920
3047
  return typeof v === "number" || v === null ? v : NA;
2921
3048
  }
2922
3049
 
3050
+ function evalFootprintCall(fname: string | null, node: Call, env: Env): Value | undefined {
3051
+ if (fname == null) return undefined;
3052
+ if (fname.startsWith("footprint.")) {
3053
+ const fp = evalExpr(callArg(node.args, 0, ["id", "footprint"]), env);
3054
+ switch (fname) {
3055
+ case "footprint.buy_volume":
3056
+ return footprintBuyVolume(fp);
3057
+ case "footprint.sell_volume":
3058
+ return footprintSellVolume(fp);
3059
+ case "footprint.delta":
3060
+ return footprintDelta(fp);
3061
+ case "footprint.total_volume":
3062
+ return footprintTotalVolume(fp);
3063
+ case "footprint.vah":
3064
+ return footprintVah(fp);
3065
+ case "footprint.val":
3066
+ return footprintVal(fp);
3067
+ case "footprint.poc":
3068
+ return footprintPoc(fp);
3069
+ case "footprint.rows": {
3070
+ const arr = new PineArray();
3071
+ for (const row of footprintRows(fp)) arr.push(row);
3072
+ return arr;
3073
+ }
3074
+ case "footprint.get_row_by_price":
3075
+ return footprintGetRowByPrice(fp, unwrap(evalExpr(callArg(node.args, 1, ["price"]), env)));
3076
+ default:
3077
+ return undefined;
3078
+ }
3079
+ }
3080
+ if (fname.startsWith("volume_row.")) {
3081
+ const vr = evalExpr(callArg(node.args, 0, ["id", "volume_row"]), env);
3082
+ switch (fname) {
3083
+ case "volume_row.up_price":
3084
+ return volumeRowUpPrice(vr);
3085
+ case "volume_row.down_price":
3086
+ return volumeRowDownPrice(vr);
3087
+ case "volume_row.buy_volume":
3088
+ return volumeRowBuyVolume(vr);
3089
+ case "volume_row.sell_volume":
3090
+ return volumeRowSellVolume(vr);
3091
+ case "volume_row.delta":
3092
+ return volumeRowDelta(vr);
3093
+ case "volume_row.total_volume":
3094
+ return volumeRowTotalVolume(vr);
3095
+ case "volume_row.has_buy_imbalance":
3096
+ return volumeRowHasBuyImbalance(vr);
3097
+ case "volume_row.has_sell_imbalance":
3098
+ return volumeRowHasSellImbalance(vr);
3099
+ default:
3100
+ return undefined;
3101
+ }
3102
+ }
3103
+ return undefined;
3104
+ }
3105
+
2923
3106
  function evalMathCall(fname: string | null, node: Call, env: Env): Cell | undefined {
2924
3107
  if (fname === "math.abs" || fname === "abs") {
2925
3108
  return mathAbs(cellArg(node, env, 0, ["number", "x"]));
@@ -3968,11 +4151,11 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
3968
4151
  }
3969
4152
  if (fname === "ta.engulfing" || fname === "engulfing") {
3970
4153
  const msg = "ta.engulfing() requires 4 arguments: open, high, low, close";
3971
- unwrap(evalExpr(wave10aArg(0, ["open"], msg), env));
4154
+ const open = unwrap(evalExpr(wave10aArg(0, ["open"], msg), env));
3972
4155
  unwrap(evalExpr(wave10aArg(1, ["high"], msg), env));
3973
4156
  unwrap(evalExpr(wave10aArg(2, ["low"], msg), env));
3974
- unwrap(evalExpr(wave10aArg(3, ["close"], msg), env));
3975
- return env.ta.engulfing();
4157
+ const close = unwrap(evalExpr(wave10aArg(3, ["close"], msg), env));
4158
+ return env.ta.engulfing(site, open, close);
3976
4159
  }
3977
4160
  if (fname === "ta.gap_detector" || fname === "gap_detector") {
3978
4161
  const msg = "ta.gap_detector() requires 3 arguments: high, low, prev_close";
@@ -3984,15 +4167,20 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
3984
4167
  }
3985
4168
  if (fname === "ta.double_top_bottom" || fname === "double_top_bottom") {
3986
4169
  const msg = "ta.double_top_bottom() requires 3 arguments";
3987
- unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
3988
- unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
3989
- return env.ta.doubleTopBottom(unwrap(evalExpr(wave10aArg(2, ["period", "length"], msg), env)));
4170
+ const high = unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
4171
+ const low = unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
4172
+ return env.ta.doubleTopBottom(
4173
+ site,
4174
+ high,
4175
+ low,
4176
+ unwrap(evalExpr(wave10aArg(2, ["period", "length"], msg), env)),
4177
+ );
3990
4178
  }
3991
4179
  if (fname === "ta.inside_bar_pattern" || fname === "inside_bar_pattern") {
3992
4180
  const msg = "ta.inside_bar_pattern() requires 2 arguments";
3993
- unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
3994
- unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
3995
- return env.ta.insideBarPattern();
4181
+ const high = unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
4182
+ const low = unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
4183
+ return env.ta.insideBarPattern(site, high, low);
3996
4184
  }
3997
4185
  if (fname === "ta.macd_signal" || fname === "macd_signal") {
3998
4186
  const msg = "ta.macd_signal() requires 2 arguments: macd_line, signal_line";
@@ -4012,6 +4200,7 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4012
4200
  if (fname === "ta.stoch_smooth" || fname === "stoch_smooth") {
4013
4201
  const msg = "ta.stoch_smooth() requires 6 arguments: high, low, close, period, smooth_k, smooth_d";
4014
4202
  return env.ta.stochSmooth(
4203
+ site,
4015
4204
  unwrap(evalExpr(wave10aArg(0, ["high"], msg), env)),
4016
4205
  unwrap(evalExpr(wave10aArg(1, ["low"], msg), env)),
4017
4206
  unwrap(evalExpr(wave10aArg(2, ["close"], msg), env)),
@@ -4055,7 +4244,11 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4055
4244
  }
4056
4245
  if (fname === "ta.rsi_divergence" || fname === "rsi_divergence") {
4057
4246
  const period = unwrap(evalExpr(callArg(node.args, 1, ["period", "length"]), env));
4058
- return env.ta.rsiDivergence(wave10bList(0, ["rsi_series", "series", "source"]), period ?? 0);
4247
+ return env.ta.rsiDivergence(
4248
+ site,
4249
+ seriesLastCell(evalExpr(callArg(node.args, 0, ["rsi_series", "series", "source"]), env)),
4250
+ period ?? 0,
4251
+ );
4059
4252
  }
4060
4253
  if (fname === "ta.cumulative_delta" || fname === "cumulative_delta") {
4061
4254
  const period = unwrap(evalExpr(callArg(node.args, 2, ["period", "length"]), env));
@@ -4102,6 +4295,366 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
4102
4295
  );
4103
4296
  }
4104
4297
  // wave10b-end
4298
+ // wave11-start — advanced / statistical ta.* surface
4299
+ const wave11 = (index: number, names: string[], message: string): Value =>
4300
+ unwrap(evalExpr(wave10aArg(index, names, message), env));
4301
+ const wave11n = (index: number, names: string[], message: string): Cell =>
4302
+ num(wave11(index, names, message));
4303
+ if (fname === "ta.sma_weighted" || fname === "sma_weighted") {
4304
+ const msg = "ta.sma_weighted() requires at least 2 arguments: series, period";
4305
+ const weightArg = callArg(node.args, 2, ["weight_type", "weight"]);
4306
+ return env.ta.smaWeighted(
4307
+ site,
4308
+ wave11n(0, ["series", "source"], msg),
4309
+ wave11n(1, ["period", "length"], msg),
4310
+ weightArg == null ? "linear" : evalExpr(weightArg, env),
4311
+ );
4312
+ }
4313
+ if (fname === "ta.apo" || fname === "apo") {
4314
+ const msg = "ta.apo() requires 3 arguments: series, fast_period, slow_period";
4315
+ return env.ta.apo(
4316
+ site,
4317
+ wave11n(0, ["series", "source"], msg),
4318
+ wave11n(1, ["fast_period", "fast"], msg),
4319
+ wave11n(2, ["slow_period", "slow"], msg),
4320
+ );
4321
+ }
4322
+ if (fname === "ta.atr_normalized" || fname === "atr_normalized") {
4323
+ const msg = "ta.atr_normalized() requires 4 arguments: high, low, close, period";
4324
+ return env.ta.atrNormalized(
4325
+ site,
4326
+ wave11n(0, ["high"], msg),
4327
+ wave11n(1, ["low"], msg),
4328
+ wave11n(2, ["close"], msg),
4329
+ wave11n(3, ["period", "length"], msg),
4330
+ );
4331
+ }
4332
+ if (fname === "ta.beta" || fname === "beta") {
4333
+ const msg = "ta.beta() requires 3 arguments: series1, series2, length";
4334
+ return env.ta.beta(
4335
+ site,
4336
+ wave11n(0, ["series1", "series", "source"], msg),
4337
+ wave11n(1, ["series2", "series_b", "other"], msg),
4338
+ wave11n(2, ["length", "period"], msg),
4339
+ );
4340
+ }
4341
+ if (fname === "ta.r_squared" || fname === "r_squared") {
4342
+ const msg = "ta.r_squared() requires 3 arguments: series1, series2, length";
4343
+ return env.ta.rSquared(
4344
+ site,
4345
+ wave11n(0, ["series1", "series", "source"], msg),
4346
+ wave11n(1, ["series2", "series_b", "other"], msg),
4347
+ wave11n(2, ["length", "period"], msg),
4348
+ );
4349
+ }
4350
+ if (fname === "ta.comovement" || fname === "comovement") {
4351
+ const msg = "ta.comovement() requires 3 arguments: series1, series2, length";
4352
+ return env.ta.comovement(
4353
+ site,
4354
+ wave11n(0, ["series1", "series", "source"], msg),
4355
+ wave11n(1, ["series2", "series_b", "other"], msg),
4356
+ wave11n(2, ["length", "period"], msg),
4357
+ );
4358
+ }
4359
+ if (fname === "ta.skewness" || fname === "skewness") {
4360
+ const msg = "ta.skewness() requires 2 arguments: series, period";
4361
+ return env.ta.skewness(
4362
+ site,
4363
+ wave11n(0, ["series", "source"], msg),
4364
+ wave11n(1, ["period", "length"], msg),
4365
+ );
4366
+ }
4367
+ if (fname === "ta.kurtosis" || fname === "kurtosis") {
4368
+ const msg = "ta.kurtosis() requires 2 arguments: series, period";
4369
+ return env.ta.kurtosis(
4370
+ site,
4371
+ wave11n(0, ["series", "source"], msg),
4372
+ wave11n(1, ["period", "length"], msg),
4373
+ );
4374
+ }
4375
+ if (fname === "ta.volume_weighted_momentum" || fname === "volume_weighted_momentum") {
4376
+ const msg = "ta.volume_weighted_momentum() requires 3 arguments: series, volume, length";
4377
+ return env.ta.volumeWeightedMomentum(
4378
+ site,
4379
+ wave11n(0, ["series", "source"], msg),
4380
+ wave11n(1, ["volume"], msg),
4381
+ wave11n(2, ["length", "period"], msg),
4382
+ );
4383
+ }
4384
+ if (fname === "ta.parkinson" || fname === "parkinson") {
4385
+ const msg = "ta.parkinson() requires 2 arguments: high, low";
4386
+ const highArg = wave10aArg(0, ["high"], msg);
4387
+ const lowArg = wave10aArg(1, ["low"], msg);
4388
+ if (isSeriesExpr(highArg, env) || isSeriesExpr(lowArg, env)) return NA;
4389
+ return env.ta.parkinson(num(evalExpr(highArg, env)), num(evalExpr(lowArg, env)));
4390
+ }
4391
+ if (fname === "ta.garman_klass" || fname === "garman_klass") {
4392
+ const msg = "ta.garman_klass() requires 4 arguments: high, low, close, open";
4393
+ const argsExpr = [
4394
+ wave10aArg(0, ["high"], msg),
4395
+ wave10aArg(1, ["low"], msg),
4396
+ wave10aArg(2, ["close"], msg),
4397
+ wave10aArg(3, ["open"], msg),
4398
+ ];
4399
+ if (argsExpr.some((a) => isSeriesExpr(a, env))) return NA;
4400
+ return env.ta.garmanKlass(
4401
+ num(evalExpr(argsExpr[0]!, env)),
4402
+ num(evalExpr(argsExpr[1]!, env)),
4403
+ num(evalExpr(argsExpr[2]!, env)),
4404
+ num(evalExpr(argsExpr[3]!, env)),
4405
+ );
4406
+ }
4407
+ if (fname === "ta.mean_reversion_score" || fname === "mean_reversion_score") {
4408
+ const msg = "ta.mean_reversion_score() requires 4 arguments";
4409
+ return env.ta.meanReversionScore(
4410
+ site,
4411
+ wave11n(0, ["close", "source"], msg),
4412
+ wave11n(1, ["sma"], msg),
4413
+ wave11n(2, ["stdev", "std_dev"], msg),
4414
+ wave11n(3, ["period", "length"], msg),
4415
+ );
4416
+ }
4417
+ if (fname === "ta.market_condition" || fname === "market_condition") {
4418
+ const msg = "ta.market_condition() requires 4 arguments";
4419
+ return env.ta.marketCondition(
4420
+ site,
4421
+ wave11n(0, ["close", "source"], msg),
4422
+ wave11n(1, ["atr"], msg),
4423
+ wave11n(2, ["sma_period", "sma_length"], msg),
4424
+ wave11n(3, ["stdev_period", "stdev_length"], msg),
4425
+ );
4426
+ }
4427
+ if (fname === "ta.order_flow_imbalance" || fname === "order_flow_imbalance") {
4428
+ const msg = "ta.order_flow_imbalance() requires 5 arguments";
4429
+ return env.ta.orderFlowImbalance(
4430
+ site,
4431
+ wave11n(0, ["high"], msg),
4432
+ wave11n(1, ["low"], msg),
4433
+ wave11n(2, ["close"], msg),
4434
+ wave11n(3, ["volume"], msg),
4435
+ wave11n(4, ["period", "length"], msg),
4436
+ );
4437
+ }
4438
+ if (fname === "ta.liquidity_score" || fname === "liquidity_score") {
4439
+ const msg = "ta.liquidity_score() requires 4 arguments";
4440
+ return env.ta.liquidityScore(
4441
+ site,
4442
+ wave11n(0, ["volume"], msg),
4443
+ wave11n(1, ["volatility"], msg),
4444
+ wave11n(2, ["spread"], msg),
4445
+ wave11n(3, ["period", "length"], msg),
4446
+ );
4447
+ }
4448
+ if (fname === "ta.fear_greed_index" || fname === "fear_greed_index") {
4449
+ const msg = "ta.fear_greed_index() requires 4 arguments";
4450
+ return env.ta.fearGreedIndex(
4451
+ wave11n(0, ["rsi"], msg),
4452
+ wave11n(1, ["vix"], msg),
4453
+ wave11n(2, ["put_call", "put_call_ratio"], msg),
4454
+ wave11n(3, ["breadth"], msg),
4455
+ );
4456
+ }
4457
+ if (fname === "ta.crowd_sentiment" || fname === "crowd_sentiment") {
4458
+ const msg = "ta.crowd_sentiment() requires 3 arguments";
4459
+ return env.ta.crowdSentiment(
4460
+ wave11n(0, ["price_agreement", "price"], msg),
4461
+ wave11n(1, ["volume_agreement", "volume"], msg),
4462
+ wave11n(2, ["time_agreement", "time"], msg),
4463
+ );
4464
+ }
4465
+ if (fname === "ta.smart_money_flow" || fname === "smart_money_flow") {
4466
+ const msg = "ta.smart_money_flow() requires 4 arguments";
4467
+ return env.ta.smartMoneyFlow(
4468
+ wave11n(0, ["price_change"], msg),
4469
+ wave11n(1, ["volume"], msg),
4470
+ wave11n(2, ["time_since_high", "time_high"], msg),
4471
+ wave11n(3, ["time_since_low", "time_low"], msg),
4472
+ );
4473
+ }
4474
+ if (fname === "ta.trend_confirmation_score" || fname === "trend_confirmation_score") {
4475
+ const msg = "ta.trend_confirmation_score() requires 6 arguments";
4476
+ wave11n(5, ["support_distance"], msg); // required, unused (Python parity)
4477
+ return env.ta.trendConfirmationScore(
4478
+ wave11n(0, ["momentum"], msg),
4479
+ wave11n(1, ["trend_alignment"], msg),
4480
+ wave11n(2, ["strength"], msg),
4481
+ wave11n(3, ["rsi"], msg),
4482
+ wave11n(4, ["rsi_alignment"], msg),
4483
+ );
4484
+ }
4485
+ if (fname === "ta.max_loss_level" || fname === "max_loss_level") {
4486
+ const msg = "ta.max_loss_level() requires 3 arguments";
4487
+ return env.ta.maxLossLevel(
4488
+ wave11n(0, ["entry", "entry_price"], msg),
4489
+ wave11n(1, ["account", "account_size"], msg),
4490
+ wave11n(2, ["max_loss_pct", "max_loss_percent"], msg),
4491
+ );
4492
+ }
4493
+ if (fname === "ta.profit_lock_level" || fname === "profit_lock_level") {
4494
+ const msg = "ta.profit_lock_level() requires 4 arguments";
4495
+ return env.ta.profitLockLevel(
4496
+ wave11n(0, ["entry", "entry_price"], msg),
4497
+ wave11n(1, ["current", "current_price"], msg),
4498
+ wave11n(2, ["trail_pct", "trail_percent"], msg),
4499
+ wave11n(3, ["direction"], msg),
4500
+ );
4501
+ }
4502
+ // wave11-end
4503
+ // wave12-start — risk / sizing / timing analytics
4504
+ const wave12s = (index: number, names: string[], message: string): Value =>
4505
+ evalExpr(wave10aArg(index, names, message), env);
4506
+ if (fname === "ta.position_sizing" || fname === "position_sizing") {
4507
+ const msg = "ta.position_sizing() requires 4 arguments";
4508
+ return env.ta.positionSizing(
4509
+ wave11n(0, ["account", "account_size"], msg),
4510
+ wave11n(1, ["risk_pct", "risk_percent"], msg),
4511
+ wave11n(2, ["entry", "entry_price"], msg),
4512
+ wave11n(3, ["stop", "stop_price"], msg),
4513
+ );
4514
+ }
4515
+ if (fname === "ta.kelly_criterion" || fname === "kelly_criterion") {
4516
+ const msg = "ta.kelly_criterion() requires 3 arguments";
4517
+ return env.ta.kellyCriterion(
4518
+ wave11n(0, ["win_rate"], msg),
4519
+ wave11n(1, ["avg_win"], msg),
4520
+ wave11n(2, ["avg_loss"], msg),
4521
+ );
4522
+ }
4523
+ if (fname === "ta.risk_reward_ratio" || fname === "risk_reward_ratio") {
4524
+ const msg = "ta.risk_reward_ratio() requires 3 arguments";
4525
+ return env.ta.riskRewardRatio(
4526
+ wave11n(0, ["entry", "entry_price"], msg),
4527
+ wave11n(1, ["stop", "stop_price"], msg),
4528
+ wave11n(2, ["target", "target_price"], msg),
4529
+ );
4530
+ }
4531
+ if (fname === "ta.risk_reward_asymmetry" || fname === "risk_reward_asymmetry") {
4532
+ const msg = "ta.risk_reward_asymmetry() requires 4 arguments";
4533
+ return env.ta.riskRewardAsymmetry(
4534
+ wave11n(0, ["entry", "entry_price"], msg),
4535
+ wave11n(1, ["stop", "stop_price"], msg),
4536
+ wave11n(2, ["target", "target_price"], msg),
4537
+ wave11n(3, ["win_rate"], msg),
4538
+ );
4539
+ }
4540
+ if (fname === "ta.probability_of_movement" || fname === "probability_of_movement") {
4541
+ const msg = "ta.probability_of_movement() requires 4 arguments";
4542
+ return env.ta.probabilityOfMovement(
4543
+ wave11n(0, ["current", "current_price"], msg),
4544
+ wave11n(1, ["target", "target_price"], msg),
4545
+ wave11n(2, ["atr"], msg),
4546
+ wave11n(3, ["period", "length"], msg),
4547
+ );
4548
+ }
4549
+ if (fname === "ta.trend_strength" || fname === "trend_strength") {
4550
+ const msg = "ta.trend_strength() requires 3 arguments";
4551
+ return env.ta.trendStrength(
4552
+ wave11n(0, ["price", "source"], msg),
4553
+ wave11n(1, ["adx"], msg),
4554
+ wave11n(2, ["rsi"], msg),
4555
+ );
4556
+ }
4557
+ if (fname === "ta.breakeven_level" || fname === "breakeven_level") {
4558
+ const msg = "ta.breakeven_level() requires 5 arguments";
4559
+ return env.ta.breakevenLevel(
4560
+ wave11n(0, ["entry", "entry_price"], msg),
4561
+ wave11n(1, ["quantity", "qty"], msg),
4562
+ wave11n(2, ["commission"], msg),
4563
+ wave11n(3, ["slippage"], msg),
4564
+ wave11n(4, ["direction"], msg),
4565
+ );
4566
+ }
4567
+ if (fname === "ta.market_structure_pivot" || fname === "market_structure_pivot") {
4568
+ const msg = "ta.market_structure_pivot() requires 5 arguments";
4569
+ wave11n(0, ["high"], msg);
4570
+ wave11n(1, ["low"], msg);
4571
+ const close = wave11n(2, ["close"], msg);
4572
+ wave11n(3, ["left", "left_bars"], msg);
4573
+ wave11n(4, ["right", "right_bars"], msg);
4574
+ return env.ta.marketStructurePivot(close);
4575
+ }
4576
+ if (fname === "ta.position_sizing_score" || fname === "position_sizing_score") {
4577
+ const msg = "ta.position_sizing_score() requires 4 arguments";
4578
+ return env.ta.positionSizingScore(
4579
+ wave11n(0, ["account_risk", "risk"], msg),
4580
+ wave11n(1, ["volatility"], msg),
4581
+ wave11n(2, ["conviction"], msg),
4582
+ wave11n(3, ["correlation"], msg),
4583
+ );
4584
+ }
4585
+ if (fname === "ta.trailing_exit_level" || fname === "trailing_exit_level") {
4586
+ const msg = "ta.trailing_exit_level() requires 5 arguments";
4587
+ return env.ta.trailingExitLevel(
4588
+ wave11n(0, ["entry", "entry_price"], msg),
4589
+ wave11n(1, ["current", "current_price"], msg),
4590
+ wave11n(2, ["atr"], msg),
4591
+ wave11n(3, ["multiplier", "mult"], msg),
4592
+ wave11n(4, ["step"], msg),
4593
+ );
4594
+ }
4595
+ if (fname === "ta.optimal_entry_zone" || fname === "optimal_entry_zone") {
4596
+ const msg = "ta.optimal_entry_zone() requires 4 arguments";
4597
+ wave11n(0, ["high"], msg);
4598
+ wave11n(1, ["low"], msg);
4599
+ const close = wave11n(2, ["close"], msg);
4600
+ wave11n(3, ["period", "length"], msg);
4601
+ return env.ta.optimalEntryZone(close);
4602
+ }
4603
+ if (fname === "ta.mean_reversion_entry" || fname === "mean_reversion_entry") {
4604
+ const msg = "ta.mean_reversion_entry() requires 5 arguments";
4605
+ return env.ta.meanReversionEntry(
4606
+ wave11n(0, ["price", "source"], msg),
4607
+ wave11n(1, ["sma"], msg),
4608
+ wave11n(2, ["stdev", "std_dev"], msg),
4609
+ wave11n(4, ["threshold"], msg),
4610
+ );
4611
+ }
4612
+ if (fname === "ta.multi_timeframe_signal" || fname === "multi_timeframe_signal") {
4613
+ const msg = "ta.multi_timeframe_signal() requires 6 arguments";
4614
+ return env.ta.multiTimeframeSignal(
4615
+ wave11n(0, ["s1", "signal1"], msg),
4616
+ wave11n(1, ["s2", "signal2"], msg),
4617
+ wave11n(2, ["s3", "signal3"], msg),
4618
+ wave11n(3, ["w1", "weight1"], msg),
4619
+ wave11n(4, ["w2", "weight2"], msg),
4620
+ wave11n(5, ["w3", "weight3"], msg),
4621
+ );
4622
+ }
4623
+ if (fname === "ta.market_timing_index" || fname === "market_timing_index") {
4624
+ const msg = "ta.market_timing_index() requires 4 arguments";
4625
+ return env.ta.marketTimingIndex(
4626
+ wave11n(0, ["trend"], msg),
4627
+ wave11n(1, ["momentum"], msg),
4628
+ wave11n(2, ["volatility"], msg),
4629
+ wave11n(3, ["sentiment"], msg),
4630
+ );
4631
+ }
4632
+ if (fname === "ta.regime_adaptive_signal" || fname === "regime_adaptive_signal") {
4633
+ const msg = "ta.regime_adaptive_signal() requires 4 arguments";
4634
+ const strength = wave11n(0, ["signal_strength", "strength"], msg);
4635
+ const volRegime = wave12s(1, ["volatility_regime", "vol_regime"], msg);
4636
+ const trendRegime = wave12s(2, ["trend_regime"], msg);
4637
+ wave11n(3, ["period", "length"], msg);
4638
+ return env.ta.regimeAdaptiveSignal(strength, volRegime, trendRegime);
4639
+ }
4640
+ if (fname === "ta.strategy_score" || fname === "strategy_score") {
4641
+ const msg = "ta.strategy_score() requires 4 arguments";
4642
+ return env.ta.strategyScore(
4643
+ wave11n(0, ["rsi"], msg),
4644
+ wave11n(1, ["macd"], msg),
4645
+ wave11n(2, ["ema_cross"], msg),
4646
+ wave11n(3, ["trend"], msg),
4647
+ );
4648
+ }
4649
+ if (fname === "ta.breakout_detection" || fname === "breakout_detection") {
4650
+ const msg = "ta.breakout_detection() requires 3 arguments";
4651
+ return env.ta.breakoutDetection(
4652
+ wave11n(0, ["close", "source"], msg),
4653
+ wave11n(1, ["resistance"], msg),
4654
+ wave11n(2, ["support"], msg),
4655
+ );
4656
+ }
4657
+ // wave12-end
4105
4658
  return undefined;
4106
4659
  }
4107
4660
 
@@ -4113,6 +4666,16 @@ function hlc3(env: Env): Cell {
4113
4666
  return (h + l + c) / 3;
4114
4667
  }
4115
4668
 
4669
+ /** Python `_series_last`: array → last cell; scalar → the value itself. */
4670
+ function seriesLastCell(value: Value): number | null {
4671
+ if (value instanceof PineArray) {
4672
+ const n = value.size();
4673
+ return n > 0 ? value.get(n - 1) : null;
4674
+ }
4675
+ const v = unwrap(value);
4676
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
4677
+ }
4678
+
4116
4679
  function asArray(value: Value): PineArray | null {
4117
4680
  if (value instanceof PineArray) return value;
4118
4681
  if (value && typeof value === "object" && "__array" in value) {
@@ -4978,16 +5541,37 @@ function evalDrawingCall(fname: string | null, node: Call, env: Env): Value | un
4978
5541
  return book.boxNew(env.barIndex, extra);
4979
5542
  }
4980
5543
  if (fname === "alert") {
5544
+ // Python `_builtin_alert`: zero-arg `alert()` is a soft no-op.
5545
+ if (asArgs(node.args).length === 0) return NA;
4981
5546
  const msg = evalAlertMessage(callArg(node.args, 0, ["message"]), env);
4982
- book.alert(env.barIndex, msg);
5547
+ const freqExpr = callArg(node.args, 1, ["freq"]);
5548
+ let freq: unknown = null;
5549
+ if (freqExpr != null) {
5550
+ const v = evalExpr(freqExpr, env);
5551
+ freq = typeof v === "string" ? v : unwrap(v);
5552
+ }
5553
+ emitAlert(env, msg, freq, { source: "alert" });
4983
5554
  return NA;
4984
5555
  }
4985
5556
  if (fname === "table.new") {
5557
+ // Pine positional order is (position, columns, rows, bgcolor,
5558
+ // frame_color, frame_width, border_color, border_width, force_overlay).
5559
+ // Python `_handle_table_new` SoT (columns then rows, not rows then columns).
4986
5560
  const extra: Record<string, unknown> = {};
4987
5561
  const position = drawRaw(node, env, 0, ["position"]);
4988
5562
  if (position !== undefined) extra.position = position;
4989
- putFinite(extra, "rows", drawNum(node, env, 1, ["rows"]));
4990
- putFinite(extra, "columns", drawNum(node, env, 2, ["columns"]));
5563
+ putFinite(extra, "columns", drawNum(node, env, 1, ["columns"]));
5564
+ putFinite(extra, "rows", drawNum(node, env, 2, ["rows"]));
5565
+ const bgcolor = drawRaw(node, env, 3, ["bgcolor"]);
5566
+ if (bgcolor !== undefined) extra.bgcolor = bgcolor;
5567
+ const frameColor = drawRaw(node, env, 4, ["frame_color"]);
5568
+ if (frameColor !== undefined) extra.frame_color = frameColor;
5569
+ putFinite(extra, "frame_width", drawNum(node, env, 5, ["frame_width"]));
5570
+ const borderColor = drawRaw(node, env, 6, ["border_color"]);
5571
+ if (borderColor !== undefined) extra.border_color = borderColor;
5572
+ putFinite(extra, "border_width", drawNum(node, env, 7, ["border_width"]));
5573
+ const forceOverlay = drawRaw(node, env, 8, ["force_overlay"]);
5574
+ if (forceOverlay !== undefined) extra.force_overlay = forceOverlay;
4991
5575
  return book.tableNew(env.barIndex, extra);
4992
5576
  }
4993
5577
  if (fname === "polyline.new") {
@@ -6028,9 +6612,10 @@ function syminfoPrefix(node: Call, env: Env): string {
6028
6612
  }
6029
6613
 
6030
6614
  function calendarMs(node: Call, env: Env): number | null {
6615
+ // Python `_timestamp_to_ms`: chart hosts store opens in seconds.
6031
6616
  const arg = callArg(node.args, 0, ["time", "timestamp"]);
6032
- if (arg == null) return num(env.ctx.time);
6033
- return unwrap(evalExpr(arg, env));
6617
+ if (arg == null) return timestampToMs(num(env.ctx.time));
6618
+ return timestampToMs(unwrap(evalExpr(arg, env)));
6034
6619
  }
6035
6620
 
6036
6621
  function evalLibraryMemberCall(mod: LibraryModule, attr: string, node: Call, env: Env): Value {