@hoox-sh/pynets 0.1.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +20 -9
  3. package/dist/browser.js +95 -34
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +23002 -12427
  6. package/package.json +1 -1
  7. package/src/ast/builder.ts +235 -12
  8. package/src/ast/nodes.ts +114 -4
  9. package/src/ast/unparse.ts +192 -17
  10. package/src/cli/rich.ts +4 -3
  11. package/src/cli.ts +29 -7
  12. package/src/generated/PinescriptLexer.interp +5 -1
  13. package/src/generated/PinescriptLexer.tokens +97 -95
  14. package/src/generated/PinescriptLexer.ts +512 -502
  15. package/src/generated/PinescriptParser.interp +8 -1
  16. package/src/generated/PinescriptParser.tokens +97 -95
  17. package/src/generated/PinescriptParser.ts +1907 -1287
  18. package/src/generated/PinescriptParserVisitor.ts +36 -1
  19. package/src/index.ts +22 -0
  20. package/src/runtime/array.ts +238 -68
  21. package/src/runtime/compile/collections.ts +492 -0
  22. package/src/runtime/compile/draw.ts +181 -0
  23. package/src/runtime/compile/emit.ts +1421 -0
  24. package/src/runtime/compile/emit_call.ts +1796 -0
  25. package/src/runtime/compile/emit_udf.ts +326 -0
  26. package/src/runtime/compile/engine.ts +265 -0
  27. package/src/runtime/compile/helpers.ts +140 -0
  28. package/src/runtime/compile/index.ts +33 -0
  29. package/src/runtime/compile/runtime.ts +357 -0
  30. package/src/runtime/compile/strategy.ts +250 -0
  31. package/src/runtime/compile/types.ts +127 -0
  32. package/src/runtime/compile/udt_runtime.ts +50 -0
  33. package/src/runtime/drawings.ts +405 -8
  34. package/src/runtime/interpret.ts +2387 -281
  35. package/src/runtime/matrix.ts +371 -42
  36. package/src/runtime/strategy.ts +695 -33
  37. package/src/runtime/ta.ts +654 -20
  38. package/src/runtime/timeframe.ts +199 -0
@@ -7,7 +7,8 @@
7
7
  * arrays, color/str, strategy events + position series, input*, math.*,
8
8
  * request.security, UDF FunctionDef, matrix.*, extra TA, drawings/alert,
9
9
  * derived prices, calendar, Pine na-compare.
10
- * Not a port of runtime/host.py — same public envelope, interpret only.
10
+ * Not a port of runtime/host.py — same public envelope.
11
+ * Compile is JS emit in ./compile/ (mode compile|auto).
11
12
  */
12
13
  import { parse } from "../ast/helper.ts";
13
14
  import type {
@@ -15,6 +16,7 @@ import type {
15
16
  Arg,
16
17
  Assign,
17
18
  Attribute,
19
+ AugAssign,
18
20
  BoolOp,
19
21
  Call,
20
22
  Conditional,
@@ -24,6 +26,7 @@ import type {
24
26
  TypeDef,
25
27
  EnumDef,
26
28
  Import,
29
+ Once,
27
30
  Param,
28
31
  ReAssign,
29
32
  Script,
@@ -58,13 +61,18 @@ import {
58
61
  import {
59
62
  timeframeFromSeconds,
60
63
  timeframeInSeconds,
64
+ timeframePeriodChanged,
61
65
  timeframeIsDaily,
66
+ timeframeIsDwm,
67
+ timeframeIsHours,
62
68
  timeframeIsIntraday,
69
+ timeframeIsMinutes,
63
70
  timeframeIsMonthly,
71
+ timeframeIsSeconds,
64
72
  timeframeIsWeekly,
65
73
  timeframeMultiplier as tfMultiplier,
66
74
  } from "./timeframe.ts";
67
- import { DrawingBook, type DrawingEvent } from "./drawings.ts";
75
+ import { DrawingBook, type DrawingEvent, type DrawingKind } from "./drawings.ts";
68
76
  import { LogBook, formatLogParts, runtimeError, type LogRecord } from "./log.ts";
69
77
  import { MemoryProvider, type BarProvider } from "./provider.ts";
70
78
  import {
@@ -137,10 +145,24 @@ import {
137
145
  strTrim,
138
146
  strUpper,
139
147
  } from "./str.ts";
140
- import { StrategyBook, type BrokerSettings, type StrategyEvent, type StrategySummary } from "./strategy.ts";
148
+ import {
149
+ StrategyBook,
150
+ isStrategyCashAmount,
151
+ strategyCashAmount,
152
+ type BrokerSettings,
153
+ type StrategyCashAmount,
154
+ type StrategyEvent,
155
+ type StrategySummary,
156
+ } from "./strategy.ts";
141
157
  import { PineMap } from "./map.ts";
142
158
  import { PineMatrix } from "./matrix.ts";
143
- import { TaEngine } from "./ta.ts";
159
+ import { TaEngine, type TaRecord } from "./ta.ts";
160
+ import {
161
+ CompileError,
162
+ compileEligible,
163
+ compileScript,
164
+ } from "./compile/index.ts";
165
+ import type { RuntimeMode } from "./compile/types.ts";
144
166
 
145
167
  export type { DrawingEvent };
146
168
 
@@ -153,15 +175,28 @@ export interface OHLCVBar {
153
175
  close?: number;
154
176
  volume?: number;
155
177
  time?: number;
178
+ time_close?: number;
156
179
  }
157
180
 
158
181
  export type InputOverrides = Record<string, number | string | boolean>;
159
182
 
183
+ /** One library source, same shape as Python `Runtime.run(..., libraries=[{...}])`. */
184
+ export type LibrarySourceSpec = {
185
+ namespace: string;
186
+ name: string;
187
+ version?: number;
188
+ source: string;
189
+ };
190
+
160
191
  export interface RuntimeOptions {
161
192
  inputs?: InputOverrides;
162
193
  broker?: BrokerSettings;
163
194
  timeframe?: string | null;
164
- libraries?: LibraryRegistry;
195
+ libraries?: LibraryRegistry | LibrarySourceSpec[];
196
+ /** interpret (default) | compile (JS emit) | auto (compile, fallback interpret). */
197
+ mode?: RuntimeMode;
198
+ /** Wall-clock budget in seconds. Interpret only; checked every 32 bars. */
199
+ timeout_seconds?: number | null;
165
200
  }
166
201
 
167
202
  export interface RuntimeFill {
@@ -180,7 +215,9 @@ export interface RuntimeResult {
180
215
  count: number;
181
216
  script_name: string | null;
182
217
  script_type: string;
183
- mode: "interpret";
218
+ mode: "interpret" | "compile";
219
+ auto_backend?: "interpret" | "compile";
220
+ compile_fallback_reason?: string;
184
221
  events?: StrategyEvent[];
185
222
  fills?: RuntimeFill[];
186
223
  drawings?: DrawingEvent[];
@@ -189,6 +226,8 @@ export interface RuntimeResult {
189
226
  plot_data?: Record<string, { data: Array<{ value: number | null; time?: number }> }>;
190
227
  /** Book-level strategy scalars when the script is a strategy (or any fills ran). */
191
228
  strategy?: StrategySummary;
229
+ /** True when interpret stopped after `timeout_seconds`. */
230
+ timed_out?: boolean;
192
231
  error?: string;
193
232
  error_kind?: string;
194
233
  }
@@ -196,8 +235,17 @@ export interface RuntimeResult {
196
235
  /** Eval-only: Cell, tagged tuple/array, color, or strategy.long/short strings. */
197
236
  type TupleVal = { __tuple: true; elts: Value[] };
198
237
  type ArrayVal = { __array: true; elts: Value[] };
238
+ type ChartPointVal = {
239
+ __chartPoint: true;
240
+ index: number | null;
241
+ time: number | null;
242
+ x: number | null;
243
+ y: number | null;
244
+ price: number | null;
245
+ };
199
246
  type Value =
200
247
  | Cell
248
+ | boolean // Python True/False (e.g. `barmerge.*`); coerces to 1/0 via unwrap/valuesEq
201
249
  | TupleVal
202
250
  | ArrayVal
203
251
  | Color
@@ -211,7 +259,10 @@ type Value =
211
259
  | LibraryModule
212
260
  | UdfDef
213
261
  | EnumType
214
- | EnumMember;
262
+ | EnumMember
263
+ | TaRecord
264
+ | ChartPointVal
265
+ | StrategyCashAmount;
215
266
 
216
267
  const LOOP_BREAK = Object.freeze({ __loop: "break" as const });
217
268
  const LOOP_CONTINUE = Object.freeze({ __loop: "continue" as const });
@@ -283,6 +334,11 @@ interface Env {
283
334
  barTimes: number[];
284
335
  libraries: LibraryRegistry;
285
336
  pendingExports: Map<string, Value>;
337
+ onceFired: WeakSet<object>;
338
+ /** Python `_max_bars_back_decls` — recorded only; evaluation is unbounded. */
339
+ maxBarsBackDecls: Array<{ var: Value; num: Value }>;
340
+ /** Python `_alert_fire_bars` keys for `alertcondition` once_per_bar. */
341
+ alertFireBars: Map<string, number>;
286
342
  }
287
343
 
288
344
  export class Runtime {
@@ -290,6 +346,8 @@ export class Runtime {
290
346
  readonly broker: BrokerSettings;
291
347
  readonly timeframe: string | null;
292
348
  readonly libraries: LibraryRegistry;
349
+ readonly mode: RuntimeMode;
350
+ readonly timeout_seconds: number | null;
293
351
 
294
352
  constructor(
295
353
  public readonly symbol = "AAPL",
@@ -298,7 +356,9 @@ export class Runtime {
298
356
  this.inputs = options?.inputs ?? {};
299
357
  this.broker = options?.broker ?? {};
300
358
  this.timeframe = options?.timeframe ?? null;
301
- this.libraries = options?.libraries ?? new LibraryRegistry();
359
+ this.libraries = resolveLibraries(options?.libraries);
360
+ this.mode = options?.mode ?? "interpret";
361
+ this.timeout_seconds = options?.timeout_seconds ?? null;
302
362
  }
303
363
 
304
364
  /** Store Pine source for `import namespace/name/version`. */
@@ -328,14 +388,24 @@ export class Runtime {
328
388
  };
329
389
  }
330
390
  const extraOpts = extra && !isPlainInputs(extra) ? extra : undefined;
391
+ const mode = extraOpts?.mode ?? this.mode;
392
+ const inputs = mergeInputs(this.inputs, extra);
393
+ const host = {
394
+ symbol: this.symbol,
395
+ inputs,
396
+ broker: extraOpts?.broker ?? this.broker,
397
+ timeframe: extraOpts?.timeframe ?? this.timeframe,
398
+ libraries: extraOpts?.libraries != null ? resolveLibraries(extraOpts.libraries) : this.libraries,
399
+ timeout_seconds: extraOpts?.timeout_seconds ?? this.timeout_seconds,
400
+ };
401
+ if (mode === "compile") {
402
+ return runCompiled(source, ohlcv, host);
403
+ }
404
+ if (mode === "auto") {
405
+ return runAuto(source, tree, ohlcv, host);
406
+ }
331
407
  try {
332
- return interpretTree(tree, ohlcv, {
333
- symbol: this.symbol,
334
- inputs: mergeInputs(this.inputs, extra),
335
- broker: extraOpts?.broker ?? this.broker,
336
- timeframe: extraOpts?.timeframe ?? this.timeframe,
337
- libraries: extraOpts?.libraries ?? this.libraries,
338
- });
408
+ return interpretTree(tree, ohlcv, host);
339
409
  } catch (err) {
340
410
  return {
341
411
  series: {},
@@ -434,6 +504,7 @@ export function interpretTree(
434
504
  broker?: BrokerSettings;
435
505
  timeframe?: string | null;
436
506
  libraries?: LibraryRegistry;
507
+ timeout_seconds?: number | null;
437
508
  },
438
509
  ): RuntimeResult {
439
510
  const scriptNode = tree.kind === "Script" ? (tree as Script) : null;
@@ -469,12 +540,25 @@ export function interpretTree(
469
540
  barTimes: ohlcv.map((b, i) => b.time ?? i * 60_000),
470
541
  libraries: host?.libraries ?? new LibraryRegistry(),
471
542
  pendingExports: new Map(),
543
+ onceFired: new WeakSet(),
544
+ maxBarsBackDecls: [],
545
+ alertFireBars: new Map(),
472
546
  };
473
547
 
474
548
  const n = ohlcv.length;
475
549
  let runError: { error: string; error_kind: string } | undefined;
550
+ let timedOut = false;
551
+ let barsDone = n;
552
+ const timeoutSec = host?.timeout_seconds;
553
+ const deadline =
554
+ timeoutSec != null && Number.isFinite(timeoutSec) ? performance.now() + timeoutSec * 1000 : null;
476
555
  try {
477
556
  for (let i = 0; i < n; i++) {
557
+ if (deadline != null && (i & 31) === 0 && performance.now() > deadline) {
558
+ timedOut = true;
559
+ barsDone = i;
560
+ break;
561
+ }
478
562
  beginBar(env, ohlcv[i]!, i);
479
563
  env.book.processPending(i, {
480
564
  open: num(env.ctx.open) ?? undefined,
@@ -526,7 +610,7 @@ export function interpretTree(
526
610
  series: plots,
527
611
  plots: firstTitle ? (plots[firstTitle] ?? []) : [],
528
612
  plot_meta,
529
- count: n,
613
+ count: timedOut ? barsDone : n,
530
614
  script_name,
531
615
  script_type,
532
616
  mode: "interpret",
@@ -539,6 +623,9 @@ export function interpretTree(
539
623
  ? { strategy: env.book.summary(num(env.ctx.close) ?? 0) }
540
624
  : {}),
541
625
  ...(runError ?? {}),
626
+ ...(timedOut
627
+ ? { timed_out: true, error: "Script execution timed out", error_kind: "runtime" }
628
+ : {}),
542
629
  };
543
630
  }
544
631
 
@@ -582,9 +669,24 @@ function beginBar(env: Env, bar: OHLCVBar, i: number): void {
582
669
  pushNamedSeries(env, "close", c);
583
670
  pushNamedSeries(env, "volume", vol);
584
671
  pushNamedSeries(env, "time", t);
672
+ // Explicit `time_close` wins. Otherwise Python's host: next bar open
673
+ // (0 counts as missing), and the last bar is this open + 1 day.
674
+ const explicitClose = bar.time_close;
675
+ let tClose = t;
676
+ if (typeof explicitClose === "number" && Number.isFinite(explicitClose)) {
677
+ tClose = explicitClose;
678
+ } else if (i + 1 < env.barCount) {
679
+ const next = env.barTimes[i + 1] ?? 0;
680
+ tClose = next !== 0 ? next : t;
681
+ } else {
682
+ tClose = Math.trunc(t) + 86_400_000;
683
+ }
684
+ pushNamedSeries(env, "time_close", tClose);
585
685
  env.ctx.bar_index = i;
586
686
  env.ctx.last_bar_index = env.barCount - 1;
587
687
  env.ctx.last_bar_time = env.lastBarTime;
688
+ // Python `_builtin_timenow`: host ctx, else dataset last bar time (no wall-clock).
689
+ env.ctx.timenow = env.lastBarTime;
588
690
 
589
691
  bindDerived(env, "hl2", (h + l) / 2);
590
692
  bindDerived(env, "hlc3", (h + l + c) / 3);
@@ -623,17 +725,23 @@ function bindDerived(env: Env, id: string, value: Cell): void {
623
725
  }
624
726
  }
625
727
 
728
+ function unwrapSpecialize(node: expr): expr {
729
+ let cur = node;
730
+ while (cur.kind === "Specialize") cur = cur.value;
731
+ return cur;
732
+ }
733
+
626
734
  function attrPath(node: expr): string | null {
627
- if (node.kind === "Name") return node.id;
628
- if (node.kind === "Attribute") {
629
- const base = attrPath(node.value);
630
- return base ? `${base}.${node.attr}` : node.attr;
735
+ const n = unwrapSpecialize(node);
736
+ if (n.kind === "Name") return n.id;
737
+ if (n.kind === "Attribute") {
738
+ const base = attrPath(n.value);
739
+ return base ? `${base}.${n.attr}` : n.attr;
631
740
  }
632
741
  return null;
633
742
  }
634
743
 
635
744
  function callName(node: Call): string | null {
636
- if (node.func.kind === "Name") return node.func.id;
637
745
  return attrPath(node.func);
638
746
  }
639
747
 
@@ -716,6 +824,8 @@ function callArg(
716
824
 
717
825
  function defaultPlotTitle(fname: string): string {
718
826
  if (fname === "hline") return "hline";
827
+ if (fname === "plotcandle") return "candles";
828
+ if (fname === "plotbar") return "bars";
719
829
  if (fname === "plotshape") return "shape";
720
830
  if (fname === "plotchar") return "char";
721
831
  if (fname === "plotarrow") return "arrow";
@@ -872,6 +982,61 @@ function evalAssign(stmt: Assign, env: Env): Value {
872
982
  return value;
873
983
  }
874
984
 
985
+ function applyBinOp(kind: string, leftV: Value, rightV: Value): Value {
986
+ // Python _safe_op: any na operand propagates na, before the string-concat
987
+ // fast path — otherwise `"a" + na` would stringify to "ana" instead of na.
988
+ // Checked on the raw values: strings are valid Add operands (unwrap maps
989
+ // them to NA as non-cells).
990
+ if (leftV == null || rightV == null) return NA;
991
+ if (kind === "Add" && (typeof leftV === "string" || typeof rightV === "string")) {
992
+ return stringifyVal(leftV) + stringifyVal(rightV);
993
+ }
994
+ const l = unwrap(leftV);
995
+ const r = unwrap(rightV);
996
+ if (l == null || r == null) return NA;
997
+ switch (kind) {
998
+ case "Add":
999
+ return l + r;
1000
+ case "Sub":
1001
+ return l - r;
1002
+ case "Mult":
1003
+ return l * r;
1004
+ case "Div":
1005
+ return r === 0 ? NA : l / r;
1006
+ case "Mod":
1007
+ return r === 0 ? NA : l % r;
1008
+ case "BitAnd":
1009
+ return Math.trunc(l) & Math.trunc(r);
1010
+ case "BitOr":
1011
+ return Math.trunc(l) | Math.trunc(r);
1012
+ case "BitXor":
1013
+ return Math.trunc(l) ^ Math.trunc(r);
1014
+ case "LShift":
1015
+ return Math.trunc(l) << Math.trunc(r);
1016
+ case "RShift":
1017
+ return Math.trunc(l) >> Math.trunc(r);
1018
+ }
1019
+ return NA;
1020
+ }
1021
+
1022
+ function evalAugAssign(stmt: AugAssign, env: Env): Value {
1023
+ if (stmt.target.kind === "Attribute") {
1024
+ const obj = evalExpr(stmt.target.value, env);
1025
+ const value = evalExpr(stmt.value, env);
1026
+ if (obj instanceof UdtInstance) {
1027
+ obj.set(stmt.target.attr, value);
1028
+ return value;
1029
+ }
1030
+ return NA;
1031
+ }
1032
+ if (stmt.target.kind !== "Name") return NA;
1033
+ const current = ctxGet(env, stmt.target.id);
1034
+ if (current === undefined) return NA;
1035
+ const result = applyBinOp(stmt.op.kind, current, evalExpr(stmt.value, env));
1036
+ bindName(env, stmt.target.id, result);
1037
+ return result;
1038
+ }
1039
+
875
1040
  function evalReAssign(stmt: ReAssign, env: Env): Value {
876
1041
  if (stmt.target.kind === "Tuple") {
877
1042
  const packed = evalExpr(stmt.value, env);
@@ -902,6 +1067,21 @@ function applyStrategyDecl(stmt: { kind: string; value?: expr | null }, env: Env
902
1067
  if (comm != null) settings.commission = comm;
903
1068
  if (slip != null) settings.slippage = slip;
904
1069
  if (pyr != null) settings.pyramiding = Math.trunc(pyr);
1070
+ const apm = evalExpr(callArg(stmt.value.args, -1, ["avg_price_model"]), env);
1071
+ if (typeof apm === "string") settings.avg_price_model = apm;
1072
+ const lev = unwrap(evalExpr(callArg(stmt.value.args, -1, ["leverage"]), env));
1073
+ if (lev != null) settings.leverage = lev;
1074
+ const ml = unwrap(evalExpr(callArg(stmt.value.args, -1, ["margin_long"]), env));
1075
+ if (ml != null) settings.margin_long = ml;
1076
+ const ms = unwrap(evalExpr(callArg(stmt.value.args, -1, ["margin_short"]), env));
1077
+ if (ms != null) settings.margin_short = ms;
1078
+ const dqt = evalExpr(callArg(stmt.value.args, -1, ["default_qty_type"]), env);
1079
+ if (typeof dqt === "string") settings.default_qty_type = dqt;
1080
+ else if (isStrategyCashAmount(dqt)) settings.default_qty_type = "cash";
1081
+ const dqv = unwrap(evalExpr(callArg(stmt.value.args, -1, ["default_qty_value"]), env));
1082
+ if (dqv != null) settings.default_qty_value = dqv;
1083
+ const cur = evalExpr(callArg(stmt.value.args, -1, ["currency"]), env);
1084
+ if (typeof cur === "string" && cur !== "") settings.currency = cur;
905
1085
  if (Object.keys(settings).length) env.book.configure(settings);
906
1086
  const capital = unwrap(evalExpr(callArg(stmt.value.args, -1, ["initial_capital"]), env));
907
1087
  if (capital != null && Number.isFinite(capital)) env.book.initialCapital = capital;
@@ -956,9 +1136,12 @@ function execStmt(
956
1136
  if (stmt.kind === "ReAssign") {
957
1137
  return evalReAssign(stmt as ReAssign, env);
958
1138
  }
1139
+ if (stmt.kind === "AugAssign") {
1140
+ return evalAugAssign(stmt as AugAssign, env);
1141
+ }
959
1142
  if (stmt.kind === "Break") throw LOOP_BREAK;
960
1143
  if (stmt.kind === "Continue") throw LOOP_CONTINUE;
961
- if (stmt.kind === "Switch" || stmt.kind === "While" || stmt.kind === "If") {
1144
+ if (stmt.kind === "Switch" || stmt.kind === "While" || stmt.kind === "If" || stmt.kind === "Once") {
962
1145
  return evalExpr(stmt as unknown as expr, env);
963
1146
  }
964
1147
  if (stmt.kind !== "Expr" || !("value" in stmt) || stmt.value == null) return NA;
@@ -973,12 +1156,18 @@ function execStmt(
973
1156
  fname === "plotchar" ||
974
1157
  fname === "plotarrow" ||
975
1158
  fname === "bgcolor" ||
976
- fname === "barcolor"
1159
+ fname === "barcolor" ||
1160
+ fname === "plotcandle" ||
1161
+ fname === "plotbar"
977
1162
  ) {
978
1163
  const plotArgs = asArgs(value.args);
1164
+ const ohlc = fname === "plotcandle" || fname === "plotbar";
979
1165
  const title = resolvePlotTitle(plotArgs, fname);
980
1166
  const key = uniquifyPlotKey(env, value, title);
981
- const seriesExpr = callArg(value.args, 0, ["series", "source"]) ?? plotArgs[0]?.value;
1167
+ // Python primary column is close (arg 3), not open.
1168
+ const seriesExpr = ohlc
1169
+ ? callArg(value.args, 3, ["close"])
1170
+ : (callArg(value.args, 0, ["series", "source"]) ?? plotArgs[0]?.value);
982
1171
  const cell = unwrap(evalExpr(seriesExpr, env));
983
1172
  if (!env.plots[key]) {
984
1173
  env.plots[key] = [];
@@ -1002,7 +1191,10 @@ function evalExpr(node: expr | undefined, env: Env): Value {
1002
1191
  switch (node.kind) {
1003
1192
  case "Name": {
1004
1193
  const v = ctxGet(env, node.id);
1005
- return v === undefined ? NA : v;
1194
+ if (v !== undefined) return v;
1195
+ // Python `_MATH_CONSTANTS`: omitted 1T quotes stay na.
1196
+ if (node.id === "bid" || node.id === "ask") return NA;
1197
+ return NA;
1006
1198
  }
1007
1199
  case "Constant": {
1008
1200
  const v = node.value;
@@ -1012,29 +1204,8 @@ function evalExpr(node: expr | undefined, env: Env): Value {
1012
1204
  if (v === false) return 0;
1013
1205
  return NA;
1014
1206
  }
1015
- case "BinOp": {
1016
- const leftV = evalExpr(node.left, env);
1017
- const rightV = evalExpr(node.right, env);
1018
- if (node.op.kind === "Add" && (typeof leftV === "string" || typeof rightV === "string")) {
1019
- return stringifyVal(leftV) + stringifyVal(rightV);
1020
- }
1021
- const l = unwrap(leftV);
1022
- const r = unwrap(rightV);
1023
- if (l == null || r == null) return NA;
1024
- switch (node.op.kind) {
1025
- case "Add":
1026
- return l + r;
1027
- case "Sub":
1028
- return l - r;
1029
- case "Mult":
1030
- return l * r;
1031
- case "Div":
1032
- return r === 0 ? NA : l / r;
1033
- case "Mod":
1034
- return r === 0 ? NA : l % r;
1035
- }
1036
- return NA;
1037
- }
1207
+ case "BinOp":
1208
+ return applyBinOp(node.op.kind, evalExpr(node.left, env), evalExpr(node.right, env));
1038
1209
  case "UnaryOp": {
1039
1210
  const v = unwrap(evalExpr(node.operand, env));
1040
1211
  if (node.op.kind === "Not") return isTruthy(v) ? 0 : 1;
@@ -1089,8 +1260,23 @@ function evalExpr(node: expr | undefined, env: Env): Value {
1089
1260
  return evalWhile(node, env);
1090
1261
  case "Switch":
1091
1262
  return evalSwitch(node, env);
1263
+ case "Once": {
1264
+ const onceNode = node as Once;
1265
+ if (env.onceFired.has(onceNode)) return NA;
1266
+ const t = onceNode.test == null ? 1 : unwrap(evalExpr(onceNode.test, env));
1267
+ if (!isTruthy(t)) return NA;
1268
+ for (const s of onceNode.body) execStmt(s, env);
1269
+ env.onceFired.add(onceNode);
1270
+ return NA;
1271
+ }
1092
1272
  case "BoolOp":
1093
1273
  return evalBoolOp(node, env);
1274
+ case "Qualify":
1275
+ return evalExpr(node.value, env);
1276
+ case "Specialize":
1277
+ return evalExpr(node.value, env);
1278
+ case "AugAssign":
1279
+ return evalAugAssign(node, env);
1094
1280
  }
1095
1281
  }
1096
1282
 
@@ -1167,16 +1353,27 @@ function evalSwitch(node: Switch, env: Env): Value {
1167
1353
  const subject = subjectPresent ? evalExpr(node.subject!, env) : null;
1168
1354
  for (const c of cases) {
1169
1355
  if (c.pattern == null) return execBlock(c.body, env);
1170
- if (subjectPresent) {
1171
- const pat = evalExpr(c.pattern, env);
1172
- if (valuesEq(pat, subject)) return execBlock(c.body, env);
1173
- } else if (isTruthy(unwrap(evalExpr(c.pattern, env)))) {
1174
- return execBlock(c.body, env);
1175
- }
1356
+ if (switchArmMatches(subjectPresent, subject, c.pattern, env)) return execBlock(c.body, env);
1176
1357
  }
1177
1358
  return NA;
1178
1359
  }
1179
1360
 
1361
+ /** Python `_switch_case_matches`: multi-value arms (`1, 2 =>`) match any element. */
1362
+ function switchArmMatches(
1363
+ subjectPresent: boolean,
1364
+ subject: Value | null,
1365
+ pattern: expr,
1366
+ env: Env,
1367
+ ): boolean {
1368
+ const pat = evalExpr(pattern, env);
1369
+ if (isTuple(pat)) {
1370
+ if (!subjectPresent) return pat.elts.some((p) => isTruthy(unwrap(p)));
1371
+ return pat.elts.some((p) => valuesEq(p, subject as Value));
1372
+ }
1373
+ if (subjectPresent) return valuesEq(pat, subject as Value);
1374
+ return isTruthy(unwrap(pat));
1375
+ }
1376
+
1180
1377
  function execBlock(body: stmt[] | stmt | null | undefined, env: Env): Value {
1181
1378
  let last: Value = NA;
1182
1379
  for (const s of asStmts(body)) last = execStmt(s, env);
@@ -1231,73 +1428,154 @@ function evalForIn(node: ForIn, env: Env): Value {
1231
1428
  return last;
1232
1429
  }
1233
1430
 
1234
- /** `strategy.long` / `strategy.short` → "long"/"short"; other attrs are na. */
1431
+ /**
1432
+ * Python `_MATH_CONSTANTS` static dotted keys (base.py).
1433
+ * `barmerge.*` are Python True/False booleans (ast/evaluator/base.py ~92),
1434
+ * matching compile (emit.ts) — plotting coerces to 1/0 on both backends.
1435
+ * Dynamic series (`timeframe.*` flags, `syminfo.ticker`, `bid`/`ask`) stay in Name blocks.
1436
+ */
1437
+ /* Exported for direct unit tests (typeof / parity assertions); not part of the package API. */
1438
+ export const MATH_CONSTANTS: Record<string, Value> = {
1439
+ "math.pi": Math.PI,
1440
+ "math.e": Math.E,
1441
+ "math.phi": (1 + Math.sqrt(5)) / 2,
1442
+ "math.rphi": 2 / (1 + Math.sqrt(5)),
1443
+ "format.mintick": "mintick",
1444
+ "format.percent": "percent",
1445
+ "format.volume": "volume",
1446
+ "format.price": "price",
1447
+ "text.formatting.none": "",
1448
+ "text.formatting.bold": "bold",
1449
+ "text.formatting.italic": "italic",
1450
+ "text.formatting.bold_italic": "bold italic",
1451
+ "size.auto": "auto",
1452
+ "size.tiny": 8,
1453
+ "size.small": 10,
1454
+ "size.normal": 12,
1455
+ "size.large": 16,
1456
+ "size.huge": 20,
1457
+ "order.ascending": 1,
1458
+ "order.descending": -1,
1459
+ "barmerge.gaps_on": true,
1460
+ "barmerge.gaps_off": false,
1461
+ "barmerge.lookahead_on": true,
1462
+ "barmerge.lookahead_off": false,
1463
+ "shape.arrowup": "arrowup",
1464
+ "shape.arrowdown": "arrowdown",
1465
+ "shape.circle": "circle",
1466
+ "shape.cross": "cross",
1467
+ "shape.diamond": "diamond",
1468
+ "shape.flag": "flag",
1469
+ "shape.labelup": "labelup",
1470
+ "shape.labeldown": "labeldown",
1471
+ "shape.square": "square",
1472
+ "shape.triangledown": "triangledown",
1473
+ "shape.triangleup": "triangleup",
1474
+ "shape.xcross": "xcross",
1475
+ "location.abovebar": "abovebar",
1476
+ "location.belowbar": "belowbar",
1477
+ "location.top": "top",
1478
+ "location.bottom": "bottom",
1479
+ "location.absolute": "absolute",
1480
+ "xloc.bar_index": "bar_index",
1481
+ "xloc.bar_time": "bar_time",
1482
+ "yloc.price": "price",
1483
+ "yloc.abovebar": "abovebar",
1484
+ "yloc.belowbar": "belowbar",
1485
+ "extend.none": "none",
1486
+ "extend.left": "left",
1487
+ "extend.right": "right",
1488
+ "extend.both": "both",
1489
+ // Pine display.* is a bitfield: none=0 pane=1 data_window=2 price_scale=4
1490
+ // status_line=8 all=15. Integers so `display.pane + display.data_window` works.
1491
+ "display.none": 0,
1492
+ "display.pane": 1,
1493
+ "display.all": 15,
1494
+ "display.data_window": 2,
1495
+ "display.price_scale": 4,
1496
+ "display.status_line": 8,
1497
+ "position.top_left": "top_left",
1498
+ "position.top_center": "top_center",
1499
+ "position.top_right": "top_right",
1500
+ "position.middle_left": "middle_left",
1501
+ "position.middle_center": "middle_center",
1502
+ "position.middle_right": "middle_right",
1503
+ "position.bottom_left": "bottom_left",
1504
+ "position.bottom_center": "bottom_center",
1505
+ "position.bottom_right": "bottom_right",
1506
+ "hline.style_solid": "solid",
1507
+ "hline.style_dashed": "dashed",
1508
+ "hline.style_dotted": "dotted",
1509
+ // Python plotting.py PlotStyle / `_builtin_plot_linestyle_*`.
1510
+ "plot.linestyle_solid": "linestyle_solid",
1511
+ "plot.linestyle_dashed": "linestyle_dashed",
1512
+ "plot.linestyle_dotted": "linestyle_dotted",
1513
+ "dayofweek.sunday": 1,
1514
+ "dayofweek.monday": 2,
1515
+ "dayofweek.tuesday": 3,
1516
+ "dayofweek.wednesday": 4,
1517
+ "dayofweek.thursday": 5,
1518
+ "dayofweek.friday": 6,
1519
+ "dayofweek.saturday": 7,
1520
+ "month.january": 1,
1521
+ "month.february": 2,
1522
+ "month.march": 3,
1523
+ "month.april": 4,
1524
+ "month.may": 5,
1525
+ "month.june": 6,
1526
+ "month.july": 7,
1527
+ "month.august": 8,
1528
+ "month.september": 9,
1529
+ "month.october": 10,
1530
+ "month.november": 11,
1531
+ "month.december": 12,
1532
+ };
1533
+
1534
+ function mathConstant(node: Attribute): Value | undefined {
1535
+ const path = attrPath(node);
1536
+ if (path == null || !Object.prototype.hasOwnProperty.call(MATH_CONSTANTS, path)) return undefined;
1537
+ return MATH_CONSTANTS[path];
1538
+ }
1539
+
1540
+ /** `strategy.long` / `strategy.short` → "long"/"short"; dotted `_MATH_CONSTANTS`; other attrs are na. */
1235
1541
  function evalAttribute(node: Attribute, env: Env): Value {
1542
+ const constant = mathConstant(node);
1543
+ if (constant !== undefined) return constant;
1544
+ const strat = evalStrategyAttr(node, env);
1545
+ if (strat !== undefined) return strat;
1236
1546
  if (node.value.kind === "Name") {
1237
- if (node.value.id === "strategy") {
1238
- if (node.attr === "long" || node.attr === "short") return node.attr;
1239
- if (node.attr === "position_size") return env.book.position.qty;
1240
- if (node.attr === "position_avg_price") return env.book.position.avgPrice;
1241
- if (node.attr === "netprofit") return env.book.realizedPnl;
1242
- if (node.attr === "openprofit") {
1243
- const q = env.book.position.qty;
1244
- const avg = env.book.position.avgPrice;
1245
- const mark = num(env.ctx.close);
1246
- if (q === 0 || avg == null || mark == null) return 0;
1247
- return q * (mark - avg);
1248
- }
1249
- if (node.attr === "equity") return env.book.equity(num(env.ctx.close) ?? 0);
1250
- if (node.attr === "opentrades") return env.book.opentrades;
1251
- if (node.attr === "closedtrades") return env.book.closedtrades;
1252
- if (node.attr === "wintrades") return env.book.wintrades;
1253
- if (node.attr === "losstrades") return env.book.losstrades;
1254
- if (node.attr === "eventrades") return env.book.eventrades;
1255
- if (node.attr === "grossprofit") return env.book.grossprofit;
1256
- if (node.attr === "grossloss") return env.book.grossloss;
1257
- if (node.attr === "avg_trade") return env.book.avgTrade();
1258
- if (node.attr === "avg_winning_trade") return env.book.avgWinningTrade();
1259
- if (node.attr === "avg_losing_trade") return env.book.avgLosingTrade();
1260
- if (node.attr === "netprofit_percent") {
1261
- return env.book.netprofitPercent(num(env.ctx.close) ?? 0);
1262
- }
1263
- if (node.attr === "openprofit_percent") {
1264
- return env.book.openprofitPercent(num(env.ctx.close) ?? 0);
1265
- }
1266
- if (node.attr === "grossprofit_percent") return env.book.grossprofitPercent();
1267
- if (node.attr === "grossloss_percent") return env.book.grosslossPercent();
1268
- if (node.attr === "avg_trade_percent") return env.book.avgTradePercent();
1269
- if (node.attr === "avg_winning_trade_percent") return env.book.avgWinningTradePercent();
1270
- if (node.attr === "avg_losing_trade_percent") return env.book.avgLosingTradePercent();
1271
- if (node.attr === "max_drawdown") return env.book.maxDrawdown();
1272
- if (node.attr === "max_drawdown_percent") return env.book.maxDrawdownPercent();
1273
- if (node.attr === "max_runup") return env.book.maxRunup();
1274
- if (node.attr === "max_runup_percent") return env.book.maxRunupPercent();
1275
- if (node.attr === "percent_profitable" || node.attr === "winrate") {
1276
- return env.book.percentProfitable();
1277
- }
1278
- if (node.attr === "profitfactor" || node.attr === "profit_factor") {
1279
- return env.book.profitFactor();
1280
- }
1281
- if (node.attr === "initial_capital") return env.book.initialCapital;
1282
- if (node.attr === "commission") return 0;
1283
- if (node.attr === "cash") return "cash";
1284
- if (node.attr === "fixed") return "fixed";
1285
- if (node.attr === "percent_of_equity") return "percent_of_equity";
1286
- }
1287
1547
  if (node.value.id === "syminfo") {
1288
1548
  if (node.attr === "ticker" || node.attr === "tickerid") return env.symbol;
1549
+ if (node.attr === "prefix") return extractPrefix(env.symbol);
1289
1550
  if (node.attr === "timezone") return "Etc/UTC";
1290
1551
  if (node.attr === "currency") return "USD";
1291
1552
  if (node.attr === "mintick") return 0.01;
1292
1553
  if (node.attr === "type") return "stock";
1554
+ if (node.attr === "isin") return "";
1555
+ if (node.attr === "current_contract") return NA;
1556
+ if (node.attr === "main_tickerid") return env.symbol;
1293
1557
  }
1294
1558
  if (node.value.id === "timeframe") {
1295
- if (node.attr === "period") return env.timeframe ?? "";
1296
- if (node.attr === "multiplier") return tfMultiplier(env.timeframe);
1297
- if (node.attr === "isintraday") return timeframeIsIntraday(env.timeframe) ? 1 : 0;
1298
- if (node.attr === "isdaily") return timeframeIsDaily(env.timeframe) ? 1 : 0;
1299
- if (node.attr === "isweekly") return timeframeIsWeekly(env.timeframe) ? 1 : 0;
1300
- if (node.attr === "ismonthly") return timeframeIsMonthly(env.timeframe) ? 1 : 0;
1559
+ // Python SoT: with no configured timeframe the evaluator's static
1560
+ // "Chart timeframe defaults (daily)" table applies (base.py) and
1561
+ // `_chart_period` / `_period_flags` resolve the chart to daily —
1562
+ // period "D", isdaily/isdwm true, everything else false. Normalize
1563
+ // null/empty to "D" so derivation lands on that table; a configured
1564
+ // timeframe keeps the dynamic helpers unchanged.
1565
+ const tf = env.timeframe == null || env.timeframe === "" ? "D" : env.timeframe;
1566
+ if (node.attr === "period") return tf;
1567
+ if (node.attr === "main_period") return tf;
1568
+ if (node.attr === "multiplier") return tfMultiplier(tf);
1569
+ if (node.attr === "isintraday") return timeframeIsIntraday(tf) ? 1 : 0;
1570
+ if (node.attr === "isdaily") return timeframeIsDaily(tf) ? 1 : 0;
1571
+ if (node.attr === "isweekly") return timeframeIsWeekly(tf) ? 1 : 0;
1572
+ if (node.attr === "ismonthly") return timeframeIsMonthly(tf) ? 1 : 0;
1573
+ if (node.attr === "isseconds" || node.attr === "isinseconds") {
1574
+ return timeframeIsSeconds(tf) ? 1 : 0;
1575
+ }
1576
+ if (node.attr === "isminutes") return timeframeIsMinutes(tf) ? 1 : 0;
1577
+ if (node.attr === "ishours") return timeframeIsHours(tf) ? 1 : 0;
1578
+ if (node.attr === "isdwm") return timeframeIsDwm(tf) ? 1 : 0;
1301
1579
  }
1302
1580
  if (node.value.id === "color") {
1303
1581
  const named = colorByName(node.attr);
@@ -1336,6 +1614,14 @@ function evalAttribute(node: Attribute, env: Env): Value {
1336
1614
  const taAttr = evalTaAttr(node.attr, env);
1337
1615
  if (taAttr !== undefined) return taAttr;
1338
1616
  }
1617
+ if (node.value.id === "order") {
1618
+ if (node.attr === "ascending") return 1;
1619
+ if (node.attr === "descending") return -1;
1620
+ }
1621
+ if (node.attr === "all") {
1622
+ const kind = drawingAllKind(node.value.id);
1623
+ if (kind != null) return drawingAllArray(env, kind);
1624
+ }
1339
1625
  }
1340
1626
  const obj = evalExpr(node.value, env);
1341
1627
  if (obj instanceof UdtInstance) {
@@ -1355,6 +1641,33 @@ function evalAttribute(node: Attribute, env: Env): Value {
1355
1641
  if (typeof exp === "function") return NA;
1356
1642
  return exp as Value;
1357
1643
  }
1644
+ if (isTaRecord(obj)) return taRecordField(obj, node.attr);
1645
+ if (isChartPoint(obj)) return chartPointField(obj, node.attr);
1646
+ return NA;
1647
+ }
1648
+
1649
+ function isTaRecord(value: Value): value is TaRecord {
1650
+ return typeof value === "object" && value !== null && (value as TaRecord).__taRecord === true;
1651
+ }
1652
+
1653
+ /** Brand-gated field read. Booleans plot as 1/0. Class instances never reach here. */
1654
+ function taRecordField(obj: TaRecord, attr: string): Value {
1655
+ if (
1656
+ obj instanceof UdtInstance ||
1657
+ obj instanceof EnumType ||
1658
+ obj instanceof LibraryModule ||
1659
+ obj instanceof PineArray ||
1660
+ obj instanceof PineMap ||
1661
+ obj instanceof PineMatrix
1662
+ ) {
1663
+ return NA;
1664
+ }
1665
+ if (!Object.prototype.hasOwnProperty.call(obj, attr)) return NA;
1666
+ const v = obj[attr];
1667
+ if (typeof v === "boolean") return v ? 1 : 0;
1668
+ if (typeof v === "number") return Number.isFinite(v) ? v : NA;
1669
+ if (v === null) return NA;
1670
+ if (typeof v === "string") return v;
1358
1671
  return NA;
1359
1672
  }
1360
1673
 
@@ -1389,7 +1702,7 @@ function evalTaAttr(attr: string, env: Env): Value | undefined {
1389
1702
  const src = h != null && l != null && c != null ? (h + l + c) / 3 : c;
1390
1703
  return (ta.vwap as TaEngine["vwap"]).call(env.ta, site, src, num(env.ctx.volume));
1391
1704
  }
1392
- if (attr === "pvt" && typeof ta.pvt === "function") {
1705
+ if ((attr === "pvt" || attr === "vpt") && typeof ta.pvt === "function") {
1393
1706
  return (ta.pvt as TaEngine["pvt"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
1394
1707
  }
1395
1708
  if (attr === "nvi" && typeof ta.nvi === "function") {
@@ -1398,6 +1711,41 @@ function evalTaAttr(attr: string, env: Env): Value | undefined {
1398
1711
  if (attr === "pvi" && typeof ta.pvi === "function") {
1399
1712
  return (ta.pvi as TaEngine["pvi"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
1400
1713
  }
1714
+ if (attr === "wad" && typeof ta.wad === "function") {
1715
+ return (ta.wad as TaEngine["wad"]).call(
1716
+ env.ta,
1717
+ site,
1718
+ num(env.ctx.high),
1719
+ num(env.ctx.low),
1720
+ num(env.ctx.close),
1721
+ num(env.ctx.volume),
1722
+ );
1723
+ }
1724
+ if (attr === "wvad" && typeof ta.wvad === "function") {
1725
+ return (ta.wvad as TaEngine["wvad"]).call(
1726
+ env.ta,
1727
+ site,
1728
+ num(env.ctx.high),
1729
+ num(env.ctx.low),
1730
+ num(env.ctx.close),
1731
+ num(env.ctx.volume),
1732
+ 20,
1733
+ );
1734
+ }
1735
+ if (attr === "cmf" && typeof ta.cmf === "function") {
1736
+ return (ta.cmf as TaEngine["cmf"]).call(
1737
+ env.ta,
1738
+ site,
1739
+ num(env.ctx.high),
1740
+ num(env.ctx.low),
1741
+ num(env.ctx.close),
1742
+ num(env.ctx.volume),
1743
+ 20,
1744
+ );
1745
+ }
1746
+ if (attr === "ao") {
1747
+ return env.ta.ao(site, num(env.ctx.high), num(env.ctx.low), 5, 34);
1748
+ }
1401
1749
  return undefined;
1402
1750
  }
1403
1751
 
@@ -1433,6 +1781,9 @@ function isNaVal(value: Value): boolean {
1433
1781
  }
1434
1782
 
1435
1783
  function valuesEq(left: Value, right: Value): boolean {
1784
+ // Python True/False compare as 1/0 (True == 1), e.g. `barmerge.gaps_on == 1`.
1785
+ if (typeof left === "boolean") left = left ? 1 : 0;
1786
+ if (typeof right === "boolean") right = right ? 1 : 0;
1436
1787
  if (isNaVal(left) && isNaVal(right)) return true;
1437
1788
  if (isNaVal(left) || isNaVal(right)) return false;
1438
1789
  if (left instanceof EnumMember) return left.equals(right);
@@ -1470,20 +1821,27 @@ function cmp(left: Value, op: string, right: Value): boolean | null {
1470
1821
  }
1471
1822
 
1472
1823
  function evalCall(node: Call, env: Env): Value {
1473
- if (node.func.kind === "Attribute") {
1474
- const obj = evalExpr(node.func.value, env);
1824
+ const callee = unwrapSpecialize(node.func);
1825
+ if (callee.kind === "Attribute") {
1826
+ const obj = evalExpr(callee.value, env);
1475
1827
  if (obj instanceof LibraryModule) {
1476
- return evalLibraryMemberCall(obj, node.func.attr, node, env);
1828
+ return evalLibraryMemberCall(obj, callee.attr, node, env);
1477
1829
  }
1478
1830
  if (obj instanceof UdtInstance) {
1479
- const meth = obj.getMethod(node.func.attr);
1831
+ const meth = obj.getMethod(callee.attr);
1480
1832
  if (meth && isUdfDef(meth.body)) return evalUdf(meth.body, node, env, obj);
1481
1833
  }
1482
- if (obj instanceof UdtType && node.func.attr === "new") {
1834
+ if (obj instanceof UdtType && callee.attr === "new") {
1483
1835
  const overrides: Record<string, unknown> = {};
1836
+ let pos = 0;
1484
1837
  for (const a of asArgs(node.args)) {
1485
1838
  const key = argKeyword(a);
1486
- if (key) overrides[key] = evalExpr(a.value, env);
1839
+ if (key) {
1840
+ overrides[key] = evalExpr(a.value, env);
1841
+ continue;
1842
+ }
1843
+ const field = obj.fields[pos++];
1844
+ if (field) overrides[field.name] = evalExpr(a.value, env);
1487
1845
  }
1488
1846
  return obj.newInstance(overrides);
1489
1847
  }
@@ -1503,7 +1861,10 @@ function evalCall(node: Call, env: Env): Value {
1503
1861
  return env.ta.rma(site, srcArg(node, env), lenArg(node, env, 1));
1504
1862
  }
1505
1863
  if (fname === "ta.atr" || fname === "atr") {
1506
- const period = lenArg(node, env, 0);
1864
+ // Python 1-arg period-only form gates the length on _is_period_like
1865
+ // (volatility.py ~54 / basic.py ~420): non-period-like → ignored → same
1866
+ // as the arg being absent (na period 0 → na kernel).
1867
+ const period = periodLikeArg(node, env, 0, ["length"]) ?? 0;
1507
1868
  return env.ta.atr(site, num(env.ctx.high), num(env.ctx.low), num(env.ctx.close), period);
1508
1869
  }
1509
1870
  if (fname === "ta.kc" || fname === "kc") {
@@ -1520,7 +1881,12 @@ function evalCall(node: Call, env: Env): Value {
1520
1881
  const mathVal = evalMathCall(fname, node, env);
1521
1882
  if (mathVal !== undefined) return mathVal;
1522
1883
  if (isInputBuiltin(fname)) return evalInput(node, env, fname!);
1523
- if (isRequestBuiltin(fname) || fname === "request.security") {
1884
+ if (
1885
+ isRequestBuiltin(fname) ||
1886
+ fname === "request.security" ||
1887
+ fname === "security" ||
1888
+ fname === "security_lower_tf"
1889
+ ) {
1524
1890
  return evalRequestCall(fname!, node, env);
1525
1891
  }
1526
1892
  const arrVal = evalArrayCall(fname, node, env);
@@ -1573,9 +1939,44 @@ function evalCall(node: Call, env: Env): Value {
1573
1939
  }
1574
1940
  const timeVal = evalTimeCall(fname, node, env);
1575
1941
  if (timeVal !== undefined) return timeVal;
1942
+ if (fname === "max_bars_back") {
1943
+ const varExpr = callArg(node.args, 0, ["var"]);
1944
+ const numExpr = callArg(node.args, 1, ["num"]);
1945
+ env.maxBarsBackDecls.push({
1946
+ var: varExpr == null ? NA : evalExpr(varExpr, env),
1947
+ num: numExpr == null ? 0 : evalExpr(numExpr, env),
1948
+ });
1949
+ return NA;
1950
+ }
1951
+ if (fname === "alertcondition") {
1952
+ const condExpr = callArg(node.args, 0, ["condition"]);
1953
+ if (condExpr == null) runtimeError("alertcondition() requires at least a condition argument");
1954
+ const raw = evalExpr(condExpr, env);
1955
+ const condition = isTruthy(unwrap(raw));
1956
+ let title = "Alert";
1957
+ let message = "Alert";
1958
+ const titleExpr = callArg(node.args, 1, ["title"]);
1959
+ const msgExpr = callArg(node.args, 2, ["message"]);
1960
+ if (titleExpr != null) {
1961
+ const t = evalExpr(titleExpr, env);
1962
+ if (t != null) title = typeof t === "string" ? t : unwrap(t) == null ? title : String(unwrap(t));
1963
+ }
1964
+ if (msgExpr != null) {
1965
+ const m = evalExpr(msgExpr, env);
1966
+ if (m != null) message = typeof m === "string" ? m : unwrap(m) == null ? message : String(unwrap(m));
1967
+ }
1968
+ if (condition) fireAlertCondition(env, title, message);
1969
+ return NA;
1970
+ }
1971
+ if (fname === "timeframe.change") return timeframeChange(node, env);
1972
+ if (fname === "syminfo.prefix") return syminfoPrefix(node, env);
1576
1973
  if (fname === "timeframe.in_seconds") {
1974
+ // Python SoT (timeframe.py:189-190): None / "" map to "D" (daily 86400)
1975
+ // before parsing — same default compile applies (runtime.ts
1976
+ // timeframeInSecondsOrDaily). A configured chart timeframe ("5") keeps
1977
+ // its own duration via the env fallback (pinned in interpret_wave6).
1577
1978
  const p = evalAsString(callArg(node.args, 0, ["timeframe", "period"]), env) ?? env.timeframe;
1578
- return timeframeInSeconds(p);
1979
+ return timeframeInSeconds(p == null || p === "" ? "D" : p);
1579
1980
  }
1580
1981
  if (fname === "timeframe.from_seconds") {
1581
1982
  const s = unwrap(evalExpr(callArg(node.args, 0, ["seconds"]), env));
@@ -1648,11 +2049,16 @@ function evalCall(node: Call, env: Env): Value {
1648
2049
  const id = evalId(callArg(node.args, 0, ["id"]));
1649
2050
  const dir = evalDirection(callArg(node.args, 1, ["direction"]), env);
1650
2051
  const qtyArg = callArg(node.args, 2, ["qty"]);
1651
- const qtyRaw = qtyArg == null ? 1 : unwrap(evalExpr(qtyArg, env));
1652
- const qty = qtyRaw == null || !Number.isFinite(qtyRaw) ? Number.NaN : qtyRaw;
2052
+ const mark = num(env.ctx.close);
2053
+ let qty: number;
2054
+ if (qtyArg == null) {
2055
+ qty = env.book.resolveDefaultQty(mark ?? 0);
2056
+ } else {
2057
+ const qtyRaw = unwrap(evalExpr(qtyArg, env));
2058
+ qty = qtyRaw == null || !Number.isFinite(qtyRaw) ? Number.NaN : qtyRaw;
2059
+ }
1653
2060
  const limit = unwrap(evalExpr(callArg(node.args, -1, ["limit"]), env));
1654
2061
  const stop = unwrap(evalExpr(callArg(node.args, -1, ["stop"]), env));
1655
- const mark = num(env.ctx.close);
1656
2062
  env.book.placeEntry(env.barIndex, id, dir, qty, {
1657
2063
  limit,
1658
2064
  stop,
@@ -1666,13 +2072,40 @@ function evalCall(node: Call, env: Env): Value {
1666
2072
  if (fname === "strategy.close") {
1667
2073
  const id = evalId(callArg(node.args, 0, ["id"]));
1668
2074
  const mark = num(env.ctx.close);
2075
+ const qtyExpr = callArg(node.args, 1, ["qty"]);
2076
+ const qtyRaw = qtyExpr == null ? undefined : unwrap(evalExpr(qtyExpr, env));
1669
2077
  if (env.book.position.qty !== 0 && mark != null) {
1670
- env.book.fillClose(env.barIndex, id, mark, { time: num(env.ctx.time) ?? undefined });
2078
+ env.book.fillClose(env.barIndex, id, mark, {
2079
+ time: num(env.ctx.time) ?? undefined,
2080
+ qty: qtyRaw ?? undefined,
2081
+ });
1671
2082
  } else env.book.close(env.barIndex, id);
1672
2083
  return NA;
1673
2084
  }
1674
2085
  if (fname === "strategy.exit") {
1675
- env.book.exit(env.barIndex, evalId(callArg(node.args, 0, ["id"])));
2086
+ const id = evalId(callArg(node.args, 0, ["id"]));
2087
+ const fromStr = evalAsString(callArg(node.args, 1, ["from_entry"]), env);
2088
+ const fromEntry = fromStr != null && fromStr !== "" ? fromStr : null;
2089
+ env.book.placeExit(env.barIndex, id, {
2090
+ from_entry: fromEntry,
2091
+ qty: unwrap(evalExpr(callArg(node.args, 2, ["qty"]), env)),
2092
+ qty_percent: unwrap(evalExpr(callArg(node.args, 3, ["qty_percent"]), env)),
2093
+ profit: unwrap(evalExpr(callArg(node.args, 4, ["profit"]), env)),
2094
+ limit: unwrap(evalExpr(callArg(node.args, 5, ["limit"]), env)),
2095
+ loss: unwrap(evalExpr(callArg(node.args, 6, ["loss"]), env)),
2096
+ stop: unwrap(evalExpr(callArg(node.args, 7, ["stop"]), env)),
2097
+ trail_price: unwrap(evalExpr(callArg(node.args, 8, ["trail_price"]), env)),
2098
+ trail_points: unwrap(evalExpr(callArg(node.args, 9, ["trail_points"]), env)),
2099
+ trail_offset: unwrap(evalExpr(callArg(node.args, 10, ["trail_offset"]), env)),
2100
+ price: num(env.ctx.close) ?? undefined,
2101
+ 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
+ },
2108
+ });
1676
2109
  return NA;
1677
2110
  }
1678
2111
  if (fname === "strategy.close_all") {
@@ -1704,6 +2137,8 @@ function evalCall(node: Call, env: Env): Value {
1704
2137
  }
1705
2138
  const riskVal = evalStrategyRiskCall(fname, node, env);
1706
2139
  if (riskVal !== undefined) return riskVal;
2140
+ const seriesVal = evalStrategySeriesCall(fname, node, env);
2141
+ if (seriesVal !== undefined) return seriesVal;
1707
2142
  const tradeVal = evalStrategyTradeCall(fname, node, env);
1708
2143
  if (tradeVal !== undefined) return tradeVal;
1709
2144
  if (fname != null && fname.endsWith(".new")) {
@@ -1730,7 +2165,11 @@ function evalKc(node: Call, env: Env, site: string): Value {
1730
2165
  let high = num(env.ctx.high);
1731
2166
  let low = num(env.ctx.low);
1732
2167
  let close = srcArg(node, env);
1733
- let length = lenArg(node, env, 1);
2168
+ // Python 3-arg reference form dispatches on _is_period_like(args[1])
2169
+ // (volatility.py ~250 / basic.py ~451): a non-period-like length is ignored
2170
+ // → same as absent (na period 0). The legacy 4-arg form below coerces via
2171
+ // _expect_int (fractional floors) and stays ungated.
2172
+ let length = periodLikeArg(node, env, 1, ["length"]) ?? 0;
1734
2173
  let mult = numArg(node, env, 2, ["mult", "multiplier"], 1);
1735
2174
  if (positional.length >= 4) {
1736
2175
  high = unwrap(evalExpr(positional[0]!.value, env));
@@ -1847,11 +2286,72 @@ function numArg(node: Call, env: Env, index: number, names: string[], fallback:
1847
2286
 
1848
2287
  function unwrap(value: Value): Cell {
1849
2288
  if (value == null) return NA;
2289
+ // Python True/False (e.g. `barmerge.*`) coerce to 1/0 like compile's `num()`
2290
+ // (True == 1) — the interpret Value pipeline is number-centric.
2291
+ if (typeof value === "boolean") return value ? 1 : 0;
1850
2292
  if (typeof value === "number") return Number.isFinite(value) ? value : NA;
2293
+ if (isStrategyCashAmount(value)) return Number.isFinite(value.value) ? value.value : NA;
1851
2294
  // Objects (PineArray / Matrix / Color / tuples) are not cells — not na.
1852
2295
  return NA;
1853
2296
  }
1854
2297
 
2298
+ /** Store array slots as-is; only coerce non-finite numbers to `na`. */
2299
+ function storeValue(value: Value): unknown {
2300
+ if (value == null) return null;
2301
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
2302
+ return value;
2303
+ }
2304
+
2305
+ function asArrayValue(value: unknown): Value {
2306
+ return value as Value;
2307
+ }
2308
+
2309
+ /** Python `_parse_sort_args` — `(id)`, `(id, order)`, `(id, order, sort_field)`, or bare field. */
2310
+ function parseSortArgs(node: Call, env: Env): { order: "asc" | "desc"; sortField: unknown } {
2311
+ const orderExpr = callArg(node.args, 1, ["order"]);
2312
+ const fieldExpr = callArg(node.args, 2, ["sort_field"]);
2313
+ const second = orderExpr == null ? undefined : evalExpr(orderExpr, env);
2314
+ const third = fieldExpr == null ? undefined : evalExpr(fieldExpr, env);
2315
+ if (fieldExpr != null) {
2316
+ return { order: isDescendingOrder(second) ? "desc" : "asc", sortField: sortFieldValue(third) };
2317
+ }
2318
+ if (orderExpr == null) return { order: "asc", sortField: null };
2319
+ if (second == null) return { order: "asc", sortField: null };
2320
+ if (typeof second === "string") {
2321
+ const low = second.toLowerCase();
2322
+ if (low === "ascending" || low === "descending" || low === "asc" || low === "desc") {
2323
+ return { order: isDescendingOrder(second) ? "desc" : "asc", sortField: null };
2324
+ }
2325
+ return { order: "asc", sortField: second };
2326
+ }
2327
+ if (typeof second === "number" && Number.isFinite(second)) {
2328
+ if (second === 1 || second === -1) {
2329
+ return { order: isDescendingOrder(second) ? "desc" : "asc", sortField: null };
2330
+ }
2331
+ return { order: "asc", sortField: second };
2332
+ }
2333
+ return { order: isDescendingOrder(second) ? "desc" : "asc", sortField: null };
2334
+ }
2335
+
2336
+ function isDescendingOrder(orderArg: Value | undefined): boolean {
2337
+ if (orderArg == null) return false;
2338
+ if (typeof orderArg === "number") return orderArg < 0;
2339
+ if (typeof orderArg === "string") return orderArg.toLowerCase().includes("desc");
2340
+ return false;
2341
+ }
2342
+
2343
+ function sortFieldValue(value: Value | undefined): unknown {
2344
+ if (value == null) return null;
2345
+ if (typeof value === "string") return value;
2346
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2347
+ return null;
2348
+ }
2349
+
2350
+ function evalSortField(node: Call, env: Env): unknown {
2351
+ const raw = evalExpr(callArg(node.args, 2, ["sort_field"]), env);
2352
+ return sortFieldValue(raw);
2353
+ }
2354
+
1855
2355
  function formatRunError(err: unknown): string {
1856
2356
  if (err instanceof Error) {
1857
2357
  const msg = (err.message || err.name || "runtime error").split("\n")[0]!.trim();
@@ -1865,7 +2365,185 @@ function formatRunError(err: unknown): string {
1865
2365
  }
1866
2366
 
1867
2367
  function isPlainInputs(extra: RuntimeOptions | InputOverrides): extra is InputOverrides {
1868
- return !("inputs" in extra) && !("broker" in extra) && !("timeframe" in extra) && !("libraries" in extra);
2368
+ return (
2369
+ !("inputs" in extra) &&
2370
+ !("broker" in extra) &&
2371
+ !("timeframe" in extra) &&
2372
+ !("libraries" in extra) &&
2373
+ !("mode" in extra) &&
2374
+ !("timeout_seconds" in extra)
2375
+ );
2376
+ }
2377
+
2378
+ function resolveLibraries(libs?: LibraryRegistry | LibrarySourceSpec[] | null): LibraryRegistry {
2379
+ if (libs instanceof LibraryRegistry) return libs;
2380
+ const reg = new LibraryRegistry();
2381
+ if (libs == null) return reg;
2382
+ for (const lib of libs) {
2383
+ if (lib == null || typeof lib !== "object") continue;
2384
+ const ns = String(lib.namespace ?? "");
2385
+ const name = String(lib.name ?? "");
2386
+ const source = String(lib.source ?? "");
2387
+ const rawVer = lib.version;
2388
+ const ver = typeof rawVer === "number" && Number.isFinite(rawVer) ? Math.trunc(rawVer) : 1;
2389
+ if (ns && name && source) reg.registerSource(ns, name, ver, source);
2390
+ }
2391
+ return reg;
2392
+ }
2393
+
2394
+ function emptyCompileResult(
2395
+ n: number,
2396
+ error: string,
2397
+ extra?: Pick<RuntimeResult, "auto_backend" | "compile_fallback_reason">,
2398
+ ): RuntimeResult {
2399
+ return {
2400
+ series: {},
2401
+ plots: [],
2402
+ plot_meta: [],
2403
+ count: n,
2404
+ script_name: null,
2405
+ script_type: "indicator",
2406
+ mode: "compile",
2407
+ error,
2408
+ error_kind: "compile",
2409
+ ...extra,
2410
+ };
2411
+ }
2412
+
2413
+ function barsToColumns(ohlcv: OHLCVBar[]): {
2414
+ open: Array<number | null>;
2415
+ high: Array<number | null>;
2416
+ low: Array<number | null>;
2417
+ close: Array<number | null>;
2418
+ volume: Array<number | null>;
2419
+ time: Array<number | null>;
2420
+ } {
2421
+ const n = ohlcv.length;
2422
+ const open: Array<number | null> = new Array(n);
2423
+ const high: Array<number | null> = new Array(n);
2424
+ const low: Array<number | null> = new Array(n);
2425
+ const close: Array<number | null> = new Array(n);
2426
+ const volume: Array<number | null> = new Array(n);
2427
+ const time: Array<number | null> = new Array(n);
2428
+ for (let i = 0; i < n; i++) {
2429
+ const b = ohlcv[i]!;
2430
+ const c = b.close ?? null;
2431
+ close[i] = c;
2432
+ open[i] = b.open ?? c;
2433
+ high[i] = b.high ?? c;
2434
+ low[i] = b.low ?? c;
2435
+ volume[i] = b.volume ?? 1;
2436
+ time[i] = b.time ?? i * 60_000;
2437
+ }
2438
+ return { open, high, low, close, volume, time };
2439
+ }
2440
+
2441
+ function packCompiled(
2442
+ source: string,
2443
+ ohlcv: OHLCVBar[],
2444
+ inputs?: InputOverrides,
2445
+ libraries?: LibraryRegistry,
2446
+ ): RuntimeResult {
2447
+ const compiled = compileScript(source, {
2448
+ getLibrary: (ns, name, ver) => libraries?.getSource(ns, name, ver) ?? null,
2449
+ });
2450
+ const cols = barsToColumns(ohlcv);
2451
+ // compileScript().run() is the raw dict (engine CompileHostResult may already strip).
2452
+ const raw = compiled.run(cols.open, cols.high, cols.low, cols.close, cols.volume, cols.time, {
2453
+ inputs,
2454
+ }) as Record<
2455
+ string,
2456
+ unknown
2457
+ > & {
2458
+ __events?: StrategyEvent[];
2459
+ __fills?: Array<{ bar: number; id: string; side: "buy" | "sell"; qty: number; price: number; type?: "fill" }>;
2460
+ __position_size?: number;
2461
+ __netprofit?: number;
2462
+ __equity?: number;
2463
+ __strategy?: StrategySummary;
2464
+ __drawings?: DrawingEvent[];
2465
+ __logs?: LogRecord[];
2466
+ };
2467
+ const events = raw.__events;
2468
+ const fillsIn = raw.__fills;
2469
+ const strategy = raw.__strategy;
2470
+ const drawings = raw.__drawings;
2471
+ const logs = raw.__logs;
2472
+ const series: Record<string, Array<number | null>> = {};
2473
+ for (const [key, value] of Object.entries(raw)) {
2474
+ if (key.startsWith("__")) continue;
2475
+ series[key] = value as Array<number | null>;
2476
+ }
2477
+ const plot_meta = compiled.plots;
2478
+ const first = plot_meta[0]?.title;
2479
+ return {
2480
+ series,
2481
+ plots: first != null ? (series[first] ?? []) : [],
2482
+ plot_meta,
2483
+ count: ohlcv.length,
2484
+ script_name: null,
2485
+ script_type: events != null || strategy != null ? "strategy" : "indicator",
2486
+ mode: "compile",
2487
+ ...(events != null ? { events } : {}),
2488
+ ...(fillsIn != null
2489
+ ? {
2490
+ fills: fillsIn.map((f) =>
2491
+ f.type === "fill" ? (f as RuntimeFill) : { type: "fill" as const, ...f },
2492
+ ),
2493
+ }
2494
+ : {}),
2495
+ ...(strategy != null ? { strategy } : {}),
2496
+ ...(drawings != null ? { drawings } : {}),
2497
+ ...(logs != null ? { logs } : {}),
2498
+ };
2499
+ }
2500
+
2501
+ function runCompiled(
2502
+ source: string,
2503
+ ohlcv: OHLCVBar[],
2504
+ host: { inputs?: InputOverrides; libraries?: LibraryRegistry },
2505
+ ): RuntimeResult {
2506
+ const elig = compileEligible(source);
2507
+ if (!elig.ok) return emptyCompileResult(ohlcv.length, elig.reason ?? "ineligible");
2508
+ try {
2509
+ return packCompiled(source, ohlcv, host.inputs, host.libraries);
2510
+ } catch (err) {
2511
+ const msg = err instanceof CompileError ? err.message : formatRunError(err);
2512
+ return emptyCompileResult(ohlcv.length, msg);
2513
+ }
2514
+ }
2515
+
2516
+ function runAuto(
2517
+ source: string,
2518
+ tree: AST,
2519
+ ohlcv: OHLCVBar[],
2520
+ host: {
2521
+ symbol?: string;
2522
+ inputs?: InputOverrides;
2523
+ broker?: BrokerSettings;
2524
+ timeframe?: string | null;
2525
+ libraries?: LibraryRegistry;
2526
+ timeout_seconds?: number | null;
2527
+ },
2528
+ ): RuntimeResult {
2529
+ const elig = compileEligible(source);
2530
+ if (elig.ok) {
2531
+ try {
2532
+ const compiled = packCompiled(source, ohlcv, host.inputs, host.libraries);
2533
+ compiled.auto_backend = "compile";
2534
+ return compiled;
2535
+ } catch (err) {
2536
+ const reason = err instanceof CompileError ? err.message : formatRunError(err);
2537
+ const out = interpretTree(tree, ohlcv, host);
2538
+ out.auto_backend = "interpret";
2539
+ out.compile_fallback_reason = reason;
2540
+ return out;
2541
+ }
2542
+ }
2543
+ const out = interpretTree(tree, ohlcv, host);
2544
+ out.auto_backend = "interpret";
2545
+ out.compile_fallback_reason = elig.reason;
2546
+ return out;
1869
2547
  }
1870
2548
 
1871
2549
  function mergeInputs(
@@ -1899,6 +2577,16 @@ function packDrawings(book: DrawingBook): DrawingEvent[] | undefined {
1899
2577
  return book.items.length === 0 ? undefined : book.items;
1900
2578
  }
1901
2579
 
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;
2584
+ env.alertFireBars.set(key, env.barIndex);
2585
+ env.drawings.alert(env.barIndex, message);
2586
+ const ev = env.drawings.items[env.drawings.items.length - 1];
2587
+ if (ev != null) ev.extra = { source: "alertcondition", title, freq: "once_per_bar" };
2588
+ }
2589
+
1902
2590
  function inputTitle(node: Call): string | null {
1903
2591
  const named = namedStringArg(node.args, "title");
1904
2592
  if (named != null) return named;
@@ -1986,6 +2674,153 @@ function evalStrategyRiskCall(fname: string | null, node: Call, env: Env): Value
1986
2674
  return undefined;
1987
2675
  }
1988
2676
 
2677
+ function evalStrategyAttr(node: Attribute, env: Env): Value | undefined {
2678
+ const path = attrPath(node);
2679
+ if (path == null || !path.startsWith("strategy.")) return undefined;
2680
+ return strategySeriesValue(path, env);
2681
+ }
2682
+
2683
+ /** Zero-arg `strategy.*` series / constants, plus identity FX stubs. */
2684
+ function evalStrategySeriesCall(fname: string | null, node: Call, env: Env): Value | undefined {
2685
+ if (fname == null || !fname.startsWith("strategy.")) return undefined;
2686
+ if (fname === "strategy.convert_to_account" || fname === "strategy.convert_to_symbol") {
2687
+ const arg = callArg(node.args, 0, ["value"]);
2688
+ if (arg == null) return 1;
2689
+ return unwrap(evalExpr(arg, env));
2690
+ }
2691
+ if (fname === "strategy.default_entry_qty") {
2692
+ const arg = callArg(node.args, 0, ["percent_equity"]);
2693
+ const pct = arg == null ? 100 : unwrap(evalExpr(arg, env));
2694
+ if (pct == null || !Number.isFinite(pct)) return NA;
2695
+ return (env.book.initialCapital * (pct / 100)) / 100;
2696
+ }
2697
+ if (fname === "strategy.opentrades.capital_held") return env.book.capitalHeld();
2698
+ return strategySeriesValue(fname, env);
2699
+ }
2700
+
2701
+ function strategySeriesValue(path: string, env: Env): Value | undefined {
2702
+ const book = env.book;
2703
+ const mark = num(env.ctx.close) ?? 0;
2704
+ switch (path) {
2705
+ case "strategy.long":
2706
+ case "strategy.direction.long":
2707
+ return "long";
2708
+ case "strategy.short":
2709
+ case "strategy.direction.short":
2710
+ return "short";
2711
+ case "strategy.direction.all":
2712
+ return "all";
2713
+ case "strategy.position_size":
2714
+ return book.position.qty;
2715
+ case "strategy.position_avg_price":
2716
+ return book.position.avgPrice;
2717
+ case "strategy.position_entry_name":
2718
+ return book.position_entry_name;
2719
+ case "strategy.netprofit":
2720
+ return book.netprofit();
2721
+ case "strategy.openprofit":
2722
+ return book.openprofit(mark);
2723
+ case "strategy.equity":
2724
+ return book.equity(mark);
2725
+ case "strategy.opentrades":
2726
+ return book.opentrades;
2727
+ case "strategy.closedtrades":
2728
+ return book.closedtrades;
2729
+ case "strategy.closedtrades.first_index":
2730
+ return book.closedtrades_first_index;
2731
+ case "strategy.wintrades":
2732
+ return book.wintrades;
2733
+ case "strategy.losstrades":
2734
+ return book.losstrades;
2735
+ case "strategy.eventrades":
2736
+ return book.eventrades;
2737
+ case "strategy.grossprofit":
2738
+ return book.grossprofit;
2739
+ case "strategy.grossloss":
2740
+ return book.grossloss;
2741
+ case "strategy.avg_trade":
2742
+ return book.avgTrade();
2743
+ case "strategy.avg_winning_trade":
2744
+ return book.avgWinningTrade();
2745
+ case "strategy.avg_losing_trade":
2746
+ return book.avgLosingTrade();
2747
+ case "strategy.netprofit_percent":
2748
+ return book.netprofitPercent(mark);
2749
+ case "strategy.openprofit_percent":
2750
+ return book.openprofitPercent(mark);
2751
+ case "strategy.grossprofit_percent":
2752
+ return book.grossprofitPercent();
2753
+ case "strategy.grossloss_percent":
2754
+ return book.grosslossPercent();
2755
+ case "strategy.avg_trade_percent":
2756
+ return book.avgTradePercent();
2757
+ case "strategy.avg_winning_trade_percent":
2758
+ return book.avgWinningTradePercent();
2759
+ case "strategy.avg_losing_trade_percent":
2760
+ return book.avgLosingTradePercent();
2761
+ case "strategy.max_drawdown":
2762
+ return book.maxDrawdown();
2763
+ case "strategy.max_drawdown_percent":
2764
+ return book.maxDrawdownPercent();
2765
+ case "strategy.max_runup":
2766
+ return book.maxRunup();
2767
+ case "strategy.max_runup_percent":
2768
+ return book.maxRunupPercent();
2769
+ case "strategy.percent_profitable":
2770
+ case "strategy.winrate":
2771
+ return book.percentProfitable();
2772
+ case "strategy.profitfactor":
2773
+ case "strategy.profit_factor":
2774
+ return book.profitFactor();
2775
+ case "strategy.initial_capital":
2776
+ return book.initialCapital;
2777
+ case "strategy.commission":
2778
+ return 0;
2779
+ case "strategy.cash":
2780
+ return strategyCashAmount(book.cash(mark));
2781
+ case "strategy.fixed":
2782
+ return "fixed";
2783
+ case "strategy.percent_of_equity":
2784
+ return "percent_of_equity";
2785
+ case "strategy.leverage":
2786
+ return book.leverage;
2787
+ case "strategy.margin_liquidation_price":
2788
+ return book.marginLiquidationPrice();
2789
+ case "strategy.avg_price_stock":
2790
+ return "stock";
2791
+ case "strategy.avg_price_futures":
2792
+ return "futures";
2793
+ case "strategy.avg_price_inverse":
2794
+ return "inverse";
2795
+ case "strategy.account_currency":
2796
+ return book.account_currency;
2797
+ case "strategy.default_entry_qty":
2798
+ return (book.initialCapital * (100 / 100)) / 100;
2799
+ case "strategy.max_contracts_held_all":
2800
+ return book.maxContractsHeldAll();
2801
+ case "strategy.max_contracts_held_long":
2802
+ return book.maxContractsHeldLong();
2803
+ case "strategy.max_contracts_held_short":
2804
+ return book.maxContractsHeldShort();
2805
+ case "strategy.opentrades.capital_held":
2806
+ return book.capitalHeld();
2807
+ case "strategy.oca.none":
2808
+ return "none";
2809
+ case "strategy.oca.cancel":
2810
+ return "cancel";
2811
+ case "strategy.oca.reduce":
2812
+ return "reduce";
2813
+ case "strategy.commission.percent":
2814
+ return "percent";
2815
+ case "strategy.commission.cash_per_order":
2816
+ return "cash_per_order";
2817
+ case "strategy.commission.cash_per_contract":
2818
+ return "cash_per_contract";
2819
+ default:
2820
+ return undefined;
2821
+ }
2822
+ }
2823
+
1989
2824
  function evalStrategyTradeCall(fname: string | null, node: Call, env: Env): Value | undefined {
1990
2825
  if (fname == null || !fname.startsWith("strategy.")) return undefined;
1991
2826
  const idx = unwrap(evalExpr(callArg(node.args, 0, ["trade_num", "trade_index", "index"]), env));
@@ -1994,32 +2829,54 @@ function evalStrategyTradeCall(fname: string | null, node: Call, env: Env): Valu
1994
2829
  const book = env.book;
1995
2830
  switch (fname) {
1996
2831
  case "strategy.closedtrades.entry_bar_index":
1997
- return book.closedEntryBar(i);
2832
+ return book.closedEntryBar(i) ?? 0;
1998
2833
  case "strategy.closedtrades.entry_price":
1999
- return book.closedEntryPrice(i);
2834
+ return book.closedEntryPrice(i) ?? 0;
2000
2835
  case "strategy.closedtrades.exit_bar_index":
2001
- return book.closedExitBar(i);
2836
+ return book.closedExitBar(i) ?? 0;
2002
2837
  case "strategy.closedtrades.exit_price":
2003
- return book.closedExitPrice(i);
2838
+ return book.closedExitPrice(i) ?? 0;
2004
2839
  case "strategy.closedtrades.profit":
2005
- return book.closedProfit(i);
2840
+ return book.closedProfit(i) ?? 0;
2006
2841
  case "strategy.closedtrades.size":
2007
- return book.closedSize(i);
2842
+ return book.closedSize(i) ?? 0;
2008
2843
  case "strategy.closedtrades.entry_id":
2009
2844
  case "strategy.closedtrades.exit_id":
2010
- return book.closedId(i);
2845
+ return book.closedId(i) ?? "";
2011
2846
  case "strategy.closedtrades.commission":
2012
- return book.closedCommission(i);
2847
+ return book.closedCommission(i) ?? 0;
2848
+ case "strategy.closedtrades.entry_time":
2849
+ return book.closedEntryTime(i);
2850
+ case "strategy.closedtrades.exit_time":
2851
+ return book.closedExitTime(i);
2852
+ case "strategy.closedtrades.entry_comment":
2853
+ return book.closedEntryComment(i);
2854
+ case "strategy.closedtrades.exit_comment":
2855
+ return book.closedExitComment(i);
2856
+ case "strategy.closedtrades.max_drawdown":
2857
+ return book.closedMaxDrawdown(i);
2858
+ case "strategy.closedtrades.max_runup":
2859
+ return book.closedMaxRunup(i);
2013
2860
  case "strategy.opentrades.entry_bar_index":
2014
- return book.openEntryBar(i);
2861
+ return book.openEntryBar(i) ?? 0;
2015
2862
  case "strategy.opentrades.entry_price":
2016
- return book.openEntryPrice(i);
2863
+ return book.openEntryPrice(i) ?? 0;
2017
2864
  case "strategy.opentrades.size":
2018
- return book.openSize(i);
2865
+ return book.openSize(i) ?? 0;
2019
2866
  case "strategy.opentrades.entry_id":
2020
- return book.openId(i);
2867
+ return book.openId(i) ?? "";
2021
2868
  case "strategy.opentrades.profit":
2022
- return book.openProfit(i, mark);
2869
+ return book.openProfit(i, mark) ?? 0;
2870
+ case "strategy.opentrades.entry_time":
2871
+ return book.openEntryTime(i);
2872
+ case "strategy.opentrades.entry_comment":
2873
+ return book.openEntryComment(i);
2874
+ case "strategy.opentrades.commission":
2875
+ return book.openCommission(i);
2876
+ case "strategy.opentrades.max_drawdown":
2877
+ return book.openMaxDrawdown(i);
2878
+ case "strategy.opentrades.max_runup":
2879
+ return book.openMaxRunup(i);
2023
2880
  default:
2024
2881
  return undefined;
2025
2882
  }
@@ -2039,7 +2896,12 @@ function evalRequestSecurity(node: Call, env: Env): Cell {
2039
2896
  }
2040
2897
 
2041
2898
  function evalRequestCall(fname: string, node: Call, env: Env): Cell {
2042
- if (fname === "request.security" || fname === "request.security_lower_tf") {
2899
+ if (
2900
+ fname === "request.security" ||
2901
+ fname === "request.security_lower_tf" ||
2902
+ fname === "security" ||
2903
+ fname === "security_lower_tf"
2904
+ ) {
2043
2905
  return evalRequestSecurity(node, env);
2044
2906
  }
2045
2907
  const host = { symbol: env.symbol, timeframe: env.timeframe };
@@ -2097,7 +2959,7 @@ function evalMathCall(fname: string | null, node: Call, env: Env): Cell | undefi
2097
2959
  if (fname === "math.todegrees") return mathToDegrees(cellArg(node, env, 0, ["radians", "x"]));
2098
2960
  if (fname === "math.toradians") return mathToRadians(cellArg(node, env, 0, ["degrees", "x"]));
2099
2961
  if (fname === "math.isfinite") return mathIsFinite(cellArg(node, env, 0, ["number", "x"]));
2100
- if (fname === "math.random") {
2962
+ if (fname === "math.random" || fname === "random") {
2101
2963
  const a0 = callArg(node.args, 0, ["min"]);
2102
2964
  const a1 = callArg(node.args, 1, ["max"]);
2103
2965
  return mathRandom(
@@ -2105,7 +2967,7 @@ function evalMathCall(fname: string | null, node: Call, env: Env): Cell | undefi
2105
2967
  a1 == null ? undefined : unwrap(evalExpr(a1, env)),
2106
2968
  );
2107
2969
  }
2108
- if (fname === "math.round_to_mintick") {
2970
+ if (fname === "math.round_to_mintick" || fname === "round_to_mintick") {
2109
2971
  const tick = typeof env.ctx["syminfo.mintick"] === "number" ? (env.ctx["syminfo.mintick"] as number) : 0.01;
2110
2972
  return mathRoundToMintick(cellArg(node, env, 0, ["number", "x"]), tick);
2111
2973
  }
@@ -2120,6 +2982,37 @@ function allCellArgs(node: Call, env: Env): Cell[] {
2120
2982
  return asArgs(node.args).map((a) => unwrap(evalExpr(a.value, env)));
2121
2983
  }
2122
2984
 
2985
+ /**
2986
+ * Python `_is_period_like` (pynescript core.py ~3427) at the cell level: only
2987
+ * whole numbers are period-like (7.0 → 7; 7.5 / na / non-numbers fail). TA
2988
+ * handlers gate period-only / period-first overload forms on this check — a
2989
+ * non-period-like length arg is IGNORED and the call behaves exactly as if
2990
+ * the arg were absent (builtin default or na-period path; same call-site
2991
+ * state key/reset behavior). Returns null when the arg must be treated as
2992
+ * absent; a passing whole number is returned as-is.
2993
+ */
2994
+ function periodLikeCell(v: Cell): number | null {
2995
+ return typeof v === "number" && Number.isInteger(v) ? v : null;
2996
+ }
2997
+
2998
+ /** `periodLikeCell` over call arg *index* (positional or kwarg *names*). */
2999
+ function periodLikeArg(node: Call, env: Env, index: number, names: string[]): number | null {
3000
+ return periodLikeCell(unwrap(evalExpr(callArg(node.args, index, names), env)));
3001
+ }
3002
+
3003
+ /** Positional (non-kwarg) argument count — detects period-only/period-first forms. */
3004
+ function positionalCount(node: Call): number {
3005
+ return asArgs(node.args).filter((a) => argKeyword(a) == null).length;
3006
+ }
3007
+
3008
+ /** True when a keyword argument named one of *names* is present. */
3009
+ function hasKwarg(node: Call, names: string[]): boolean {
3010
+ return asArgs(node.args).some((a) => {
3011
+ const kw = argKeyword(a);
3012
+ return kw != null && names.includes(kw);
3013
+ });
3014
+ }
3015
+
2123
3016
  function lenOrDefault(
2124
3017
  node: Call,
2125
3018
  env: Env,
@@ -2139,10 +3032,31 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2139
3032
  const ta = env.ta as TaAny;
2140
3033
  if (fname === "ta.highest" || fname === "highest") {
2141
3034
  if (typeof ta.highest !== "function") return NA;
3035
+ if (positionalCount(node) <= 1 && !hasKwarg(node, ["source", "series"])) {
3036
+ // Python 1-arg period-only form (basic.py:222-233 via _expect_series
3037
+ // allow_period_only): source defaults to the context high series and
3038
+ // the length gates on _is_period_like → non-period-like → na period 0.
3039
+ return (ta.highest as TaEngine["highest"]).call(
3040
+ env.ta,
3041
+ site,
3042
+ num(env.ctx.high),
3043
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
3044
+ );
3045
+ }
2142
3046
  return (ta.highest as TaEngine["highest"]).call(env.ta, site, srcArg(node, env), lenArg(node, env, 1));
2143
3047
  }
2144
3048
  if (fname === "ta.lowest" || fname === "lowest") {
2145
3049
  if (typeof ta.lowest !== "function") return NA;
3050
+ if (positionalCount(node) <= 1 && !hasKwarg(node, ["source", "series"])) {
3051
+ // Python 1-arg period-only form (basic.py:235-246): source defaults to
3052
+ // the context low series; length gated like ta.highest.
3053
+ return (ta.lowest as TaEngine["lowest"]).call(
3054
+ env.ta,
3055
+ site,
3056
+ num(env.ctx.low),
3057
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
3058
+ );
3059
+ }
2146
3060
  return (ta.lowest as TaEngine["lowest"]).call(env.ta, site, srcArg(node, env), lenArg(node, env, 1));
2147
3061
  }
2148
3062
  if (fname === "ta.stdev" || fname === "stdev") {
@@ -2194,6 +3108,20 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2194
3108
  }
2195
3109
  if (fname === "ta.bb" || fname === "bb") {
2196
3110
  if (typeof ta.bb !== "function") return NA;
3111
+ if (
3112
+ positionalCount(node) === 2 &&
3113
+ !hasKwarg(node, ["source", "series"]) &&
3114
+ periodLikeArg(node, env, 0, ["source", "series", "length"]) != null
3115
+ ) {
3116
+ // Python period-first form (basic.py:394-401): (length, mult) with the
3117
+ // series defaulting to the context close. Dispatch gates slot 0 on
3118
+ // _is_period_like, so a series first slot stays on the 3-arg form.
3119
+ const length = periodLikeArg(node, env, 0, ["source", "series", "length"])!;
3120
+ const multRaw = unwrap(evalExpr(callArg(node.args, 1, ["mult", "multiplier"]), env));
3121
+ const mult = typeof multRaw === "number" && Number.isFinite(multRaw) ? multRaw : Number.NaN;
3122
+ const r = (ta.bb as TaEngine["bb"]).call(env.ta, site, num(env.ctx.close), length, mult);
3123
+ return tupleOf([r.mid, r.up, r.lo]);
3124
+ }
2197
3125
  const r = (ta.bb as TaEngine["bb"]).call(
2198
3126
  env.ta,
2199
3127
  site,
@@ -2227,6 +3155,29 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2227
3155
  }
2228
3156
  if (fname === "ta.vwma" || fname === "vwma") {
2229
3157
  if (typeof ta.vwma !== "function") return NA;
3158
+ if (positionalCount(node) <= 1 && !hasKwarg(node, ["volume"])) {
3159
+ // Python 1-arg period-only form (basic.py:80-88): close + volume from
3160
+ // the chart context; length gates on _is_period_like → non-period-like
3161
+ // → na period 0.
3162
+ return (ta.vwma as TaEngine["vwma"]).call(
3163
+ env.ta,
3164
+ site,
3165
+ num(env.ctx.close),
3166
+ num(env.ctx.volume),
3167
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
3168
+ );
3169
+ }
3170
+ if (positionalCount(node) >= 3 || hasKwarg(node, ["volume"])) {
3171
+ // Python 3-arg community form (basic.py:89-100): (source, volume,
3172
+ // length). A period-like middle slot swaps volume back to the context
3173
+ // series (basic.py:93/97) — cells are last samples, mirroring
3174
+ // _is_period_like's series-last-sample check.
3175
+ const periodRaw = unwrap(evalExpr(callArg(node.args, 2, ["length", "period"]), env));
3176
+ const period = typeof periodRaw === "number" && Number.isFinite(periodRaw) ? periodRaw : 0;
3177
+ const volLike = periodLikeArg(node, env, 1, ["volume"]) != null;
3178
+ const vol = volLike ? num(env.ctx.volume) : unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env));
3179
+ return (ta.vwma as TaEngine["vwma"]).call(env.ta, site, srcArg(node, env), vol, period);
3180
+ }
2230
3181
  return (ta.vwma as TaEngine["vwma"]).call(
2231
3182
  env.ta,
2232
3183
  site,
@@ -2237,21 +3188,43 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2237
3188
  }
2238
3189
  if (fname === "ta.cci" || fname === "cci") {
2239
3190
  if (typeof ta.cci !== "function") return NA;
2240
- const h = num(env.ctx.high);
2241
- const l = num(env.ctx.low);
2242
- const c = srcArg(node, env);
2243
- const tp = h != null && l != null && c != null ? (h + l + c) / 3 : c;
2244
- return (ta.cci as TaEngine["cci"]).call(env.ta, site, tp, lenArg(node, env, 1));
3191
+ if (positionalCount(node) >= 4) {
3192
+ // Python legacy 4-arg form (oscillators.py:178-187): typical price of
3193
+ // the given (high, low, close) series.
3194
+ const h = unwrap(evalExpr(callArg(node.args, 0, ["high"]), env));
3195
+ const l = unwrap(evalExpr(callArg(node.args, 1, ["low"]), env));
3196
+ const c = unwrap(evalExpr(callArg(node.args, 2, ["close"]), env));
3197
+ const tp = h != null && l != null && c != null ? (h + l + c) / 3 : c;
3198
+ return (ta.cci as TaEngine["cci"]).call(env.ta, site, tp, lenArg(node, env, 3));
3199
+ }
3200
+ if (positionalCount(node) <= 1 && !hasKwarg(node, ["source", "series"])) {
3201
+ // Python 1-arg period-only form (oscillators.py:159-171): typical price
3202
+ // from the context high/low/close (hlc3 fallback); length gates on
3203
+ // _is_period_like → non-period-like → na period 0.
3204
+ return (ta.cci as TaEngine["cci"]).call(
3205
+ env.ta,
3206
+ site,
3207
+ hlc3(env),
3208
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
3209
+ );
3210
+ }
3211
+ // Python 2-arg form (oscillators.py:172-177): CCI over the source itself
3212
+ // (_cci(series, series, series, period) → tp = series). SoT correction:
3213
+ // the previous blend of context high/low into the source matched no
3214
+ // Python dispatch arm.
3215
+ return (ta.cci as TaEngine["cci"]).call(env.ta, site, srcArg(node, env), lenArg(node, env, 1));
2245
3216
  }
2246
3217
  if (fname === "ta.wpr" || fname === "wpr") {
2247
3218
  if (typeof ta.willr !== "function") return NA;
3219
+ // Python 1-arg period-only form gates length via _is_period_like
3220
+ // (oscillators.py ~198): non-period-like → ignored → na period 0.
2248
3221
  return (ta.willr as TaEngine["willr"]).call(
2249
3222
  env.ta,
2250
3223
  site,
2251
3224
  num(env.ctx.high),
2252
3225
  num(env.ctx.low),
2253
3226
  num(env.ctx.close),
2254
- lenArg(node, env, 0),
3227
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
2255
3228
  );
2256
3229
  }
2257
3230
  if (fname === "ta.willr" || fname === "willr") {
@@ -2262,28 +3235,41 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2262
3235
  num(env.ctx.high),
2263
3236
  num(env.ctx.low),
2264
3237
  num(env.ctx.close),
2265
- lenArg(node, env, 0),
3238
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
2266
3239
  );
2267
3240
  }
2268
3241
  if (fname === "ta.stoch" || fname === "stoch") {
2269
3242
  if (typeof ta.stoch !== "function") return NA;
2270
3243
  const positional = asArgs(node.args).filter((a) => argKeyword(a) == null);
2271
- const highArg = callArg(node.args, 1, ["high"]);
2272
- if (highArg == null && positional.length <= 1) {
3244
+ if (!hasKwarg(node, ["high"]) && positional.length <= 2) {
3245
+ let length: number;
3246
+ if (positional.length === 2) {
3247
+ // Python 2-arg community form (kLength, dPeriod) — both slots gate on
3248
+ // _is_period_like (oscillators.py:73-78); dPeriod is ignored and %K
3249
+ // is returned over the context close/high/low.
3250
+ const k = periodLikeArg(node, env, 0, ["length"]);
3251
+ const d = periodLikeArg(node, env, 1, ["d", "smooth"]);
3252
+ length = k != null && d != null ? k : 0;
3253
+ } else {
3254
+ // Python 1-arg period-only form gates length via _is_period_like
3255
+ // (oscillators.py ~58): non-period-like → ignored → na period 0.
3256
+ length = periodLikeArg(node, env, 0, ["length"]) ?? 0;
3257
+ }
2273
3258
  return (ta.stoch as TaEngine["stoch"]).call(
2274
3259
  env.ta,
2275
3260
  site,
2276
3261
  num(env.ctx.close),
2277
3262
  num(env.ctx.high),
2278
3263
  num(env.ctx.low),
2279
- lenArg(node, env, 0),
3264
+ length,
2280
3265
  );
2281
3266
  }
3267
+ // reference Pine form: source, high, low, length
2282
3268
  return (ta.stoch as TaEngine["stoch"]).call(
2283
3269
  env.ta,
2284
3270
  site,
2285
3271
  srcArg(node, env),
2286
- unwrap(evalExpr(highArg, env)),
3272
+ unwrap(evalExpr(callArg(node.args, 1, ["high"]), env)),
2287
3273
  unwrap(evalExpr(callArg(node.args, 2, ["low"]), env)),
2288
3274
  lenArg(node, env, 3),
2289
3275
  );
@@ -2351,6 +3337,16 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2351
3337
  }
2352
3338
  if (fname === "ta.highestbars" || fname === "highestbars") {
2353
3339
  if (typeof ta.highestbars !== "function") return NA;
3340
+ if (positionalCount(node) <= 1 && !hasKwarg(node, ["source", "series"])) {
3341
+ // Python 1-arg period-only form (basic.py:248-269): source defaults to
3342
+ // the context high series; length gates on _is_period_like.
3343
+ return (ta.highestbars as TaEngine["highestbars"]).call(
3344
+ env.ta,
3345
+ site,
3346
+ num(env.ctx.high),
3347
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
3348
+ );
3349
+ }
2354
3350
  return (ta.highestbars as TaEngine["highestbars"]).call(
2355
3351
  env.ta,
2356
3352
  site,
@@ -2360,6 +3356,16 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2360
3356
  }
2361
3357
  if (fname === "ta.lowestbars" || fname === "lowestbars") {
2362
3358
  if (typeof ta.lowestbars !== "function") return NA;
3359
+ if (positionalCount(node) <= 1 && !hasKwarg(node, ["source", "series"])) {
3360
+ // Python 1-arg period-only form (basic.py:271-291): source defaults to
3361
+ // the context low series; length gates like ta.highestbars.
3362
+ return (ta.lowestbars as TaEngine["lowestbars"]).call(
3363
+ env.ta,
3364
+ site,
3365
+ num(env.ctx.low),
3366
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
3367
+ );
3368
+ }
2363
3369
  return (ta.lowestbars as TaEngine["lowestbars"]).call(
2364
3370
  env.ta,
2365
3371
  site,
@@ -2416,7 +3422,21 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2416
3422
  }
2417
3423
  if (fname === "ta.pivothigh" || fname === "pivothigh") {
2418
3424
  if (typeof ta.pivothigh !== "function") return NA;
2419
- return (ta.pivothigh as TaEngine["pivothigh"]).call(
3425
+ if (
3426
+ positionalCount(node) === 2 &&
3427
+ !hasKwarg(node, ["source", "series"]) &&
3428
+ periodLikeArg(node, env, 0, ["leftbars", "left"]) != null &&
3429
+ periodLikeArg(node, env, 1, ["rightbars", "right"]) != null
3430
+ ) {
3431
+ // Python period-first form (basic.py:1007-1010): both slots gate on
3432
+ // _is_period_like and the source defaults to the context high series.
3433
+ // A non-period-like slot errors in Python; the repo gated-form
3434
+ // convention treats it as absent → the call falls through to na.
3435
+ const left = periodLikeArg(node, env, 0, ["leftbars", "left"])!;
3436
+ const right = periodLikeArg(node, env, 1, ["rightbars", "right"])!;
3437
+ return (ta.pivothigh as TaEngine["pivothigh"]).call(env.ta, site, num(env.ctx.high), left, right);
3438
+ }
3439
+ return (ta.pivothigh as TaEngine["pivothigh"]).call(
2420
3440
  env.ta,
2421
3441
  site,
2422
3442
  srcArg(node, env),
@@ -2426,6 +3446,18 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2426
3446
  }
2427
3447
  if (fname === "ta.pivotlow" || fname === "pivotlow") {
2428
3448
  if (typeof ta.pivotlow !== "function") return NA;
3449
+ if (
3450
+ positionalCount(node) === 2 &&
3451
+ !hasKwarg(node, ["source", "series"]) &&
3452
+ periodLikeArg(node, env, 0, ["leftbars", "left"]) != null &&
3453
+ periodLikeArg(node, env, 1, ["rightbars", "right"]) != null
3454
+ ) {
3455
+ // Python period-first form (basic.py:1036-1039): source defaults to
3456
+ // the context low series; slot gating mirrors ta.pivothigh.
3457
+ const left = periodLikeArg(node, env, 0, ["leftbars", "left"])!;
3458
+ const right = periodLikeArg(node, env, 1, ["rightbars", "right"])!;
3459
+ return (ta.pivotlow as TaEngine["pivotlow"]).call(env.ta, site, num(env.ctx.low), left, right);
3460
+ }
2429
3461
  return (ta.pivotlow as TaEngine["pivotlow"]).call(
2430
3462
  env.ta,
2431
3463
  site,
@@ -2442,6 +3474,8 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2442
3474
  if (typeof ta.mfi !== "function") return NA;
2443
3475
  const positional = asArgs(node.args).filter((a) => a.name == null);
2444
3476
  if (positional.length <= 1) {
3477
+ // Python 1-arg period-only form gates length via _is_period_like
3478
+ // (volume.py ~108): non-period-like → ignored → na period 0.
2445
3479
  return (ta.mfi as TaEngine["mfi"]).call(
2446
3480
  env.ta,
2447
3481
  site,
@@ -2449,7 +3483,7 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2449
3483
  num(env.ctx.low),
2450
3484
  num(env.ctx.close),
2451
3485
  num(env.ctx.volume),
2452
- lenArg(node, env, 0),
3486
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
2453
3487
  );
2454
3488
  }
2455
3489
  if (positional.length === 2) {
@@ -2500,13 +3534,44 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2500
3534
  }
2501
3535
  if (fname === "ta.adx" || fname === "adx") {
2502
3536
  if (typeof ta.adx !== "function") return NA;
3537
+ if (positionalCount(node) >= 4) {
3538
+ // Python legacy 4-arg form (common.py:718-726): (high, low, close,
3539
+ // length) with explicit series.
3540
+ return (ta.adx as TaEngine["adx"]).call(
3541
+ env.ta,
3542
+ site,
3543
+ srcArg(node, env),
3544
+ unwrap(evalExpr(callArg(node.args, 1, ["low"]), env)),
3545
+ unwrap(evalExpr(callArg(node.args, 2, ["close"]), env)),
3546
+ lenArg(node, env, 3),
3547
+ );
3548
+ }
3549
+ if (positionalCount(node) === 2 && !hasKwarg(node, ["high", "low", "close"])) {
3550
+ // Python 2-arg form (diLength, adxSmoothing) (common.py:709-717): both
3551
+ // slots gate on _is_period_like and the ADX period is the SECOND slot —
3552
+ // diLength is accepted but unused by the kernel dispatch. A
3553
+ // non-period-like slot → treated as absent → default 14 (same
3554
+ // convention as the 1-arg form below).
3555
+ const di = periodLikeArg(node, env, 0, ["diLength", "length"]);
3556
+ const smooth = periodLikeArg(node, env, 1, ["adxSmoothing", "adxlen"]);
3557
+ return (ta.adx as TaEngine["adx"]).call(
3558
+ env.ta,
3559
+ site,
3560
+ num(env.ctx.high),
3561
+ num(env.ctx.low),
3562
+ num(env.ctx.close),
3563
+ di != null && smooth != null ? smooth : 14,
3564
+ );
3565
+ }
3566
+ // Python 1-arg period-only form gates length via _is_period_like
3567
+ // (common.py ~701): non-period-like → ignored → default 14 (as absent).
2503
3568
  return (ta.adx as TaEngine["adx"]).call(
2504
3569
  env.ta,
2505
3570
  site,
2506
3571
  num(env.ctx.high),
2507
3572
  num(env.ctx.low),
2508
3573
  num(env.ctx.close),
2509
- lenOrDefault(node, env, 0, ["length"], 14),
3574
+ periodLikeArg(node, env, 0, ["length"]) ?? 14,
2510
3575
  );
2511
3576
  }
2512
3577
  if (fname === "ta.correlation" || fname === "correlation") {
@@ -2537,12 +3602,15 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2537
3602
  if (typeof ta.tsi !== "function") return NA;
2538
3603
  const positional = asArgs(node.args).filter((a) => a.name == null);
2539
3604
  if (positional.length === 2) {
3605
+ // Python 2-arg period-first form gates BOTH slots via _is_period_like
3606
+ // (oscillators.py ~299): args[0]=short, args[1]=long; a non-period-like
3607
+ // slot is ignored → TS default (13/25), as if the slot were absent.
2540
3608
  return (ta.tsi as TaEngine["tsi"]).call(
2541
3609
  env.ta,
2542
3610
  site,
2543
3611
  num(env.ctx.close),
2544
- lenOrDefault(node, env, 1, ["long", "long_length"], 25),
2545
- lenOrDefault(node, env, 0, ["short", "short_length"], 13),
3612
+ periodLikeArg(node, env, 1, ["long", "long_length"]) ?? 25,
3613
+ periodLikeArg(node, env, 0, ["short", "short_length"]) ?? 13,
2546
3614
  );
2547
3615
  }
2548
3616
  return (ta.tsi as TaEngine["tsi"]).call(
@@ -2651,27 +3719,47 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2651
3719
  num(env.ctx.volume),
2652
3720
  );
2653
3721
  }
2654
- if (fname === "ta.pvt" || fname === "pvt") {
3722
+ if (fname === "ta.pvt" || fname === "pvt" || fname === "ta.vpt" || fname === "vpt") {
2655
3723
  if (typeof ta.pvt !== "function") return NA;
2656
3724
  return (ta.pvt as TaEngine["pvt"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
2657
3725
  }
2658
3726
  if (fname === "ta.wad" || fname === "wad") {
2659
3727
  if (typeof ta.wad !== "function") return NA;
3728
+ const positional = asArgs(node.args).filter((a) => argKeyword(a) == null);
3729
+ if (positional.length >= 4) {
3730
+ return (ta.wad as TaEngine["wad"]).call(
3731
+ env.ta,
3732
+ site,
3733
+ unwrap(evalExpr(callArg(node.args, 0, ["high"]), env)),
3734
+ unwrap(evalExpr(callArg(node.args, 1, ["low"]), env)),
3735
+ unwrap(evalExpr(callArg(node.args, 2, ["close"]), env)),
3736
+ unwrap(evalExpr(callArg(node.args, 3, ["volume"]), env)),
3737
+ );
3738
+ }
2660
3739
  return (ta.wad as TaEngine["wad"]).call(
2661
3740
  env.ta,
2662
3741
  site,
2663
3742
  num(env.ctx.high),
2664
3743
  num(env.ctx.low),
2665
3744
  num(env.ctx.close),
3745
+ num(env.ctx.volume),
2666
3746
  );
2667
3747
  }
2668
3748
  if (fname === "ta.nvi" || fname === "nvi") {
2669
3749
  if (typeof ta.nvi !== "function") return NA;
2670
- return (ta.nvi as TaEngine["nvi"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
3750
+ const cArg = callArg(node.args, 0, ["source", "close"]);
3751
+ const vArg = callArg(node.args, 1, ["volume"]);
3752
+ const close = cArg == null ? num(env.ctx.close) : unwrap(evalExpr(cArg, env));
3753
+ const volume = vArg == null ? num(env.ctx.volume) : unwrap(evalExpr(vArg, env));
3754
+ return (ta.nvi as TaEngine["nvi"]).call(env.ta, site, close, volume);
2671
3755
  }
2672
3756
  if (fname === "ta.pvi" || fname === "pvi") {
2673
3757
  if (typeof ta.pvi !== "function") return NA;
2674
- return (ta.pvi as TaEngine["pvi"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
3758
+ const cArg = callArg(node.args, 0, ["source", "close"]);
3759
+ const vArg = callArg(node.args, 1, ["volume"]);
3760
+ const close = cArg == null ? num(env.ctx.close) : unwrap(evalExpr(cArg, env));
3761
+ const volume = vArg == null ? num(env.ctx.volume) : unwrap(evalExpr(vArg, env));
3762
+ return (ta.pvi as TaEngine["pvi"]).call(env.ta, site, close, volume);
2675
3763
  }
2676
3764
  if (fname === "ta.iii" || fname === "iii") {
2677
3765
  if (typeof ta.iii !== "function") return NA;
@@ -2686,16 +3774,98 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2686
3774
  }
2687
3775
  if (fname === "ta.wvad" || fname === "wvad") {
2688
3776
  if (typeof ta.wvad !== "function") return NA;
3777
+ const positional = asArgs(node.args).filter((a) => argKeyword(a) == null);
3778
+ if (positional.length >= 4) {
3779
+ return (ta.wvad as TaEngine["wvad"]).call(
3780
+ env.ta,
3781
+ site,
3782
+ unwrap(evalExpr(callArg(node.args, 0, ["high"]), env)),
3783
+ unwrap(evalExpr(callArg(node.args, 1, ["low"]), env)),
3784
+ unwrap(evalExpr(callArg(node.args, 2, ["close"]), env)),
3785
+ unwrap(evalExpr(callArg(node.args, 3, ["volume"]), env)),
3786
+ lenOrDefault(node, env, 4, ["length", "period"], 20),
3787
+ );
3788
+ }
2689
3789
  return (ta.wvad as TaEngine["wvad"]).call(
2690
3790
  env.ta,
2691
3791
  site,
2692
- num(env.ctx.open),
2693
3792
  num(env.ctx.high),
2694
3793
  num(env.ctx.low),
2695
3794
  num(env.ctx.close),
2696
3795
  num(env.ctx.volume),
3796
+ // Python 1-arg period-only form gates length via _is_period_like
3797
+ // (volume.py ~586/~620): non-period-like → ignored → default 20 (0-arg
3798
+ // fallback), exactly as if the arg were absent.
3799
+ periodLikeArg(node, env, 0, ["length", "period"]) ?? 20,
3800
+ );
3801
+ }
3802
+ if (fname === "ta.cmf" || fname === "cmf") {
3803
+ if (typeof ta.cmf !== "function") return NA;
3804
+ const positional = asArgs(node.args).filter((a) => argKeyword(a) == null);
3805
+ if (positional.length >= 5) {
3806
+ return (ta.cmf as TaEngine["cmf"]).call(
3807
+ env.ta,
3808
+ site,
3809
+ unwrap(evalExpr(callArg(node.args, 1, ["high"]), env)),
3810
+ unwrap(evalExpr(callArg(node.args, 2, ["low"]), env)),
3811
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
3812
+ unwrap(evalExpr(callArg(node.args, 3, ["volume"]), env)),
3813
+ lenOrDefault(node, env, 4, ["length", "period"], 20),
3814
+ );
3815
+ }
3816
+ return (ta.cmf as TaEngine["cmf"]).call(
3817
+ env.ta,
3818
+ site,
3819
+ num(env.ctx.high),
3820
+ num(env.ctx.low),
3821
+ num(env.ctx.close),
3822
+ num(env.ctx.volume),
3823
+ // Python 1-arg period-only form gates length via _is_period_like
3824
+ // (volume.py ~659/~681): non-period-like → ignored → default 20, as
3825
+ // if the arg were absent.
3826
+ periodLikeArg(node, env, 0, ["length", "period"]) ?? 20,
2697
3827
  );
2698
3828
  }
3829
+ if (fname === "ta.klinger" || fname === "klinger") {
3830
+ if (typeof ta.klinger !== "function") return NA;
3831
+ return (ta.klinger as TaEngine["klinger"]).call(
3832
+ env.ta,
3833
+ site,
3834
+ unwrap(evalExpr(callArg(node.args, 2, ["close"]), env)),
3835
+ unwrap(evalExpr(callArg(node.args, 3, ["volume"]), env)),
3836
+ lenOrDefault(node, env, 4, ["fast_period", "fast", "fastlen"], 0),
3837
+ lenOrDefault(node, env, 5, ["slow_period", "slow", "slowlen"], 0),
3838
+ );
3839
+ }
3840
+ if (fname === "ta.ao" || fname === "ta.aroon") {
3841
+ // Python parity (pynescript base.py / oscillators.py):
3842
+ // "ao"/"aroon" are absent from _TA_KWARG_ORDERS and their handlers carry no
3843
+ // _KWARG_ORDER, so kwargs fall through the legacy path of
3844
+ // _merge_kwargs_into_args (base.py ~603): kwarg VALUES are appended
3845
+ // positionally after the positional args, in kwarg iteration order —
3846
+ // the names are ignored (ta.ao(slow=10) binds fast=10; ta.ao(len=8)
3847
+ // binds fast=8; ta.aroon(3, length=5) binds length=3).
3848
+ const list = asArgs(node.args);
3849
+ const merged: Cell[] = [];
3850
+ for (const a of list) {
3851
+ if (argKeyword(a) == null) merged.push(unwrap(evalExpr(a.value, env)));
3852
+ }
3853
+ for (const a of list) {
3854
+ if (argKeyword(a) != null) merged.push(unwrap(evalExpr(argValue(a), env)));
3855
+ }
3856
+ // Python handlers gate each length slot on _is_period_like (core.py ~3427):
3857
+ // only whole numbers pass (7.0 → 7; 7.5 / bool / na / non-numbers fail).
3858
+ // A non-period-like arg is IGNORED and the builtin default is used.
3859
+ const periodLike = periodLikeCell;
3860
+ if (fname === "ta.ao") {
3861
+ const fast = merged.length >= 1 ? periodLike(merged[0]!) : null;
3862
+ const slow = merged.length >= 2 ? periodLike(merged[1]!) : null;
3863
+ return env.ta.ao(site, num(env.ctx.high), num(env.ctx.low), fast ?? 5, slow ?? 34);
3864
+ }
3865
+ const length = merged.length >= 1 ? periodLike(merged[0]!) : null;
3866
+ const r = env.ta.aroon(site, num(env.ctx.high), num(env.ctx.low), length ?? 14);
3867
+ return tupleOf([r.down, r.up]);
3868
+ }
2699
3869
  if (fname === "ta.pivot_point_levels") {
2700
3870
  if (typeof ta.pivotPoints !== "function") return NA;
2701
3871
  const r = (ta.pivotPoints as TaEngine["pivotPoints"]).call(
@@ -2707,6 +3877,80 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2707
3877
  );
2708
3878
  return tupleOf([r.pp, r.r1, r.s1, r.r2, r.s2]);
2709
3879
  }
3880
+ if (fname === "ta.uo" || fname === "uo") {
3881
+ return env.ta.uo(
3882
+ site,
3883
+ num(env.ctx.high),
3884
+ num(env.ctx.low),
3885
+ num(env.ctx.close),
3886
+ lenArg(node, env, 0),
3887
+ lenArg(node, env, 1),
3888
+ lenArg(node, env, 2),
3889
+ );
3890
+ }
3891
+ if (fname === "ta.rci" || fname === "rci") {
3892
+ return env.ta.rci(site, srcArg(node, env), lenArg(node, env, 1));
3893
+ }
3894
+ if (fname === "ta.dpo" || fname === "dpo") {
3895
+ return env.ta.dpo(site, num(env.ctx.close), lenArg(node, env, 0));
3896
+ }
3897
+ if (fname === "ta.kst" || fname === "kst") {
3898
+ return env.ta.kst(
3899
+ site,
3900
+ num(env.ctx.close),
3901
+ lenArg(node, env, 0),
3902
+ lenArg(node, env, 1),
3903
+ lenArg(node, env, 2),
3904
+ lenArg(node, env, 3),
3905
+ );
3906
+ }
3907
+ if (fname === "ta.stochrsi" || fname === "stochrsi") {
3908
+ return env.ta.stochRsi(site, num(env.ctx.close), lenArg(node, env, 0), lenArg(node, env, 1));
3909
+ }
3910
+ if (fname === "ta.donchian" || fname === "donchian") {
3911
+ return env.ta.donchian(site, num(env.ctx.high), num(env.ctx.low), lenArg(node, env, 0));
3912
+ }
3913
+ if (fname === "ta.ichimoku" || fname === "ichimoku") {
3914
+ return env.ta.ichimoku(
3915
+ site,
3916
+ num(env.ctx.high),
3917
+ num(env.ctx.low),
3918
+ lenArg(node, env, 0),
3919
+ lenArg(node, env, 1),
3920
+ );
3921
+ }
3922
+ if (fname === "ta.bb_pct" || fname === "bb_pct") {
3923
+ return env.ta.bbPct(
3924
+ site,
3925
+ num(env.ctx.close),
3926
+ lenArg(node, env, 0),
3927
+ numArg(node, env, 1, ["std_dev", "mult", "std"], 2),
3928
+ );
3929
+ }
3930
+ if (fname === "ta.emv" || fname === "emv") {
3931
+ return env.ta.emv(
3932
+ site,
3933
+ num(env.ctx.high),
3934
+ num(env.ctx.low),
3935
+ num(env.ctx.volume),
3936
+ lenArg(node, env, 0),
3937
+ );
3938
+ }
3939
+ if (fname === "ta.fractal" || fname === "fractal") {
3940
+ return env.ta.fractal(site, num(env.ctx.high), num(env.ctx.low), lenArg(node, env, 0));
3941
+ }
3942
+ if (fname === "ta.atr_stop" || fname === "atr_stop") {
3943
+ const atr = unwrap(evalExpr(callArg(node.args, 0, ["atr", "atr_value"]), env));
3944
+ const multRaw = unwrap(evalExpr(callArg(node.args, 1, ["multiplier", "mult"]), env));
3945
+ const mult = multRaw == null || !Number.isFinite(multRaw) ? 2 : multRaw;
3946
+ return env.ta.atrStop(num(env.ctx.close), atr, mult);
3947
+ }
3948
+ if (fname === "ta.zigzag" || fname === "zigzag") {
3949
+ const thRaw = unwrap(evalExpr(callArg(node.args, 1, ["threshold", "percent", "deviation"]), env));
3950
+ const th = thRaw == null || !Number.isFinite(thRaw) ? 5 : thRaw;
3951
+ const z = env.ta.zigzag(site, srcArg(node, env), th);
3952
+ return tupleOf([z.high, z.low, z.dir]);
3953
+ }
2710
3954
  return undefined;
2711
3955
  }
2712
3956
 
@@ -2723,37 +3967,74 @@ function asArray(value: Value): PineArray | null {
2723
3967
  if (value && typeof value === "object" && "__array" in value) {
2724
3968
  const elts = (value as ArrayVal).elts;
2725
3969
  const arr = new PineArray();
2726
- for (const e of elts) arr.push(unwrap(e));
3970
+ for (const e of elts) arr.push(storeValue(e));
2727
3971
  return arr;
2728
3972
  }
2729
3973
  return null;
2730
3974
  }
2731
3975
 
3976
+ /** Every `array.new*` alias Python registers in `_array_builtin_map`. */
3977
+ const ARRAY_NEW_FNS = new Set([
3978
+ "array.new",
3979
+ "array.new_bool",
3980
+ "array.new_int",
3981
+ "array.new_float",
3982
+ "array.new_string",
3983
+ "array.new_color",
3984
+ "array.newbool",
3985
+ "array.newint",
3986
+ "array.newfloat",
3987
+ "array.newstring",
3988
+ "array.newcolor",
3989
+ "array.newlabel",
3990
+ "array.newline",
3991
+ "array.newbox",
3992
+ "array.newtable",
3993
+ "array.newpolyline",
3994
+ "array.newlinefill",
3995
+ "array.new_label",
3996
+ "array.new_line",
3997
+ "array.new_box",
3998
+ "array.new_table",
3999
+ "array.new_polyline",
4000
+ "array.new_linefill",
4001
+ "array.new_chart.point",
4002
+ ]);
4003
+
4004
+ function isArrayNew(fname: string | null): boolean {
4005
+ const name = arrayBuiltin(fname);
4006
+ return name != null && ARRAY_NEW_FNS.has(name);
4007
+ }
4008
+
4009
+ function arrayBuiltin(fname: string | null): string | null {
4010
+ if (fname == null || !fname.startsWith("array.")) return null;
4011
+ const lt = fname.indexOf("<");
4012
+ return lt >= 0 ? fname.slice(0, lt) : fname;
4013
+ }
4014
+
2732
4015
  function evalArrayCall(fname: string | null, node: Call, env: Env): Value | undefined {
2733
- if (
2734
- fname === "array.new_float" ||
2735
- fname === "array.new" ||
2736
- fname === "array.new_int"
2737
- ) {
4016
+ const aname = arrayBuiltin(fname);
4017
+ if (isArrayNew(fname)) {
2738
4018
  const size = unwrap(evalExpr(callArg(node.args, 0, ["size"]), env)) ?? 0;
2739
- const initial = unwrap(evalExpr(callArg(node.args, 1, ["initial_value"]), env));
4019
+ const initialArg = callArg(node.args, 1, ["initial_value"]);
4020
+ const initial = initialArg == null ? undefined : storeValue(evalExpr(initialArg, env));
2740
4021
  return new PineArray(size, initial);
2741
4022
  }
2742
- if (fname === "array.push") {
4023
+ if (aname === "array.push") {
2743
4024
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2744
- const v = unwrap(evalExpr(callArg(node.args, 1, ["value"]), env));
4025
+ const v = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
2745
4026
  arr?.push(v);
2746
4027
  return NA;
2747
4028
  }
2748
- if (fname === "array.get") {
4029
+ if (aname === "array.get") {
2749
4030
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2750
4031
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2751
- return arr ? arr.get(i ?? 0) : NA;
4032
+ return arr ? asArrayValue(arr.get(i ?? 0)) : NA;
2752
4033
  }
2753
- if (fname === "array.set") {
4034
+ if (aname === "array.set") {
2754
4035
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2755
4036
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2756
- const v = unwrap(evalExpr(callArg(node.args, 2, ["value"]), env));
4037
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
2757
4038
  if (arr && i != null) arr.set(i, v);
2758
4039
  return NA;
2759
4040
  }
@@ -2763,7 +4044,7 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2763
4044
  }
2764
4045
  if (fname === "array.pop") {
2765
4046
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2766
- return arr ? arr.pop() : NA;
4047
+ return arr ? asArrayValue(arr.pop()) : NA;
2767
4048
  }
2768
4049
  if (fname === "array.clear") {
2769
4050
  asArray(evalExpr(callArg(node.args, 0, ["id"]), env))?.clear();
@@ -2771,40 +4052,40 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2771
4052
  }
2772
4053
  if (fname === "array.unshift") {
2773
4054
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2774
- arr?.unshift(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env)));
4055
+ arr?.unshift(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env)));
2775
4056
  return NA;
2776
4057
  }
2777
4058
  if (fname === "array.shift") {
2778
4059
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2779
- return arr ? arr.shift() : NA;
4060
+ return arr ? asArrayValue(arr.shift()) : NA;
2780
4061
  }
2781
4062
  if (fname === "array.includes") {
2782
4063
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2783
- return arr?.includes(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) ? 1 : 0;
4064
+ return arr?.includes(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env))) ? 1 : 0;
2784
4065
  }
2785
4066
  if (fname === "array.first") {
2786
4067
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2787
- return arr ? arr.first() : NA;
4068
+ return arr ? asArrayValue(arr.first()) : NA;
2788
4069
  }
2789
4070
  if (fname === "array.last") {
2790
4071
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2791
- return arr ? arr.last() : NA;
4072
+ return arr ? asArrayValue(arr.last()) : NA;
2792
4073
  }
2793
4074
  if (fname === "array.insert") {
2794
4075
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2795
4076
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2796
- const v = unwrap(evalExpr(callArg(node.args, 2, ["value"]), env));
4077
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
2797
4078
  if (arr && i != null) arr.insert(i, v);
2798
4079
  return NA;
2799
4080
  }
2800
4081
  if (fname === "array.remove") {
2801
4082
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2802
4083
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2803
- return arr && i != null ? arr.remove(i) : NA;
4084
+ return arr && i != null ? asArrayValue(arr.remove(i)) : NA;
2804
4085
  }
2805
4086
  if (fname === "array.fill") {
2806
4087
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2807
- arr?.fill(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env)));
4088
+ arr?.fill(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env)));
2808
4089
  return NA;
2809
4090
  }
2810
4091
  if (fname === "array.slice") {
@@ -2824,14 +4105,13 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2824
4105
  }
2825
4106
  if (fname === "array.sort") {
2826
4107
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2827
- const orderRaw = evalExpr(callArg(node.args, 1, ["order"]), env);
2828
- const order = typeof orderRaw === "string" && orderRaw.toLowerCase().includes("desc") ? "desc" : "asc";
2829
- arr?.sort(order);
4108
+ const parsed = parseSortArgs(node, env);
4109
+ arr?.sort(parsed.order, parsed.sortField);
2830
4110
  return NA;
2831
4111
  }
2832
4112
  if (fname === "array.indexof") {
2833
4113
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2834
- return arr ? arr.indexof(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4114
+ return arr ? arr.indexof(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
2835
4115
  }
2836
4116
  if (fname === "array.avg") {
2837
4117
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
@@ -2854,19 +4134,14 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2854
4134
  const sep = evalAsString(callArg(node.args, 1, ["separator", "sep"]), env);
2855
4135
  return arr ? arr.join(sep ?? ",") : NA;
2856
4136
  }
2857
- if (fname === "array.new_bool" || fname === "array.new_string") {
2858
- const size = unwrap(evalExpr(callArg(node.args, 0, ["size"]), env)) ?? 0;
2859
- const initial = unwrap(evalExpr(callArg(node.args, 1, ["initial_value"]), env));
2860
- return new PineArray(size, initial);
2861
- }
2862
4137
  if (fname === "array.from") {
2863
4138
  const arr = new PineArray();
2864
- for (const a of asArgs(node.args)) arr.push(unwrap(evalExpr(a.value, env)));
4139
+ for (const a of asArgs(node.args)) arr.push(storeValue(evalExpr(a.value, env)));
2865
4140
  return arr;
2866
4141
  }
2867
4142
  if (fname === "array.lastindexof") {
2868
4143
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2869
- return arr ? arr.lastIndexOf(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4144
+ return arr ? arr.lastIndexOf(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
2870
4145
  }
2871
4146
  if (fname === "array.concat") {
2872
4147
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
@@ -2899,15 +4174,18 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2899
4174
  }
2900
4175
  if (fname === "array.binary_search") {
2901
4176
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2902
- return arr ? arr.binarySearch(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4177
+ const value = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4178
+ return arr ? arr.binarySearch(value, evalSortField(node, env)) : NA;
2903
4179
  }
2904
4180
  if (fname === "array.binary_search_leftmost") {
2905
4181
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2906
- return arr ? arr.binarySearchLeftmost(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4182
+ const value = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4183
+ return arr ? arr.binarySearchLeftmost(value, evalSortField(node, env)) : NA;
2907
4184
  }
2908
4185
  if (fname === "array.binary_search_rightmost") {
2909
4186
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2910
- return arr ? arr.binarySearchRightmost(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4187
+ const value = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4188
+ return arr ? arr.binarySearchRightmost(value, evalSortField(node, env)) : NA;
2911
4189
  }
2912
4190
  if (fname === "array.stdev") {
2913
4191
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
@@ -2943,10 +4221,9 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2943
4221
  }
2944
4222
  if (fname === "array.sort_indices") {
2945
4223
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2946
- const orderRaw = evalExpr(callArg(node.args, 1, ["order"]), env);
2947
- const order = typeof orderRaw === "string" && orderRaw.toLowerCase().includes("desc") ? "desc" : "asc";
4224
+ const parsed = parseSortArgs(node, env);
2948
4225
  if (!arr) return NA;
2949
- const idx = arr.sortIndices(order);
4226
+ const idx = arr.sortIndices(parsed.order, parsed.sortField);
2950
4227
  if (idx == null) return NA;
2951
4228
  const out = new PineArray();
2952
4229
  for (const i of idx) out.push(i);
@@ -3048,20 +4325,20 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3048
4325
  const rows = unwrap(evalExpr(callArg(node.args, 0, ["rows"]), env)) ?? 0;
3049
4326
  const cols = unwrap(evalExpr(callArg(node.args, 1, ["columns", "cols"]), env)) ?? 0;
3050
4327
  const initialArg = callArg(node.args, 2, ["initial_value", "initial"]);
3051
- const initial = initialArg == null ? undefined : unwrap(evalExpr(initialArg, env));
4328
+ const initial = initialArg == null ? undefined : storeValue(evalExpr(initialArg, env));
3052
4329
  return new PineMatrix(rows, cols, initial);
3053
4330
  }
3054
4331
  if (name === "matrix.get") {
3055
4332
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3056
4333
  const row = unwrap(evalExpr(callArg(node.args, 1, ["row"]), env));
3057
4334
  const col = unwrap(evalExpr(callArg(node.args, 2, ["column", "col"]), env));
3058
- return m ? m.get(row ?? 0, col ?? 0) : NA;
4335
+ return m ? asArrayValue(m.get(row ?? 0, col ?? 0)) : NA;
3059
4336
  }
3060
4337
  if (name === "matrix.set") {
3061
4338
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3062
4339
  const row = unwrap(evalExpr(callArg(node.args, 1, ["row"]), env));
3063
4340
  const col = unwrap(evalExpr(callArg(node.args, 2, ["column", "col"]), env));
3064
- const v = unwrap(evalExpr(callArg(node.args, 3, ["value"]), env));
4341
+ const v = storeValue(evalExpr(callArg(node.args, 3, ["value"]), env));
3065
4342
  if (m && row != null && col != null) m.set(row, col, v);
3066
4343
  return NA;
3067
4344
  }
@@ -3075,7 +4352,7 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3075
4352
  }
3076
4353
  if (name === "matrix.fill") {
3077
4354
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3078
- const v = unwrap(evalExpr(callArg(node.args, 1, ["value"]), env));
4355
+ const v = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
3079
4356
  m?.fill(v);
3080
4357
  return NA;
3081
4358
  }
@@ -3274,14 +4551,115 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3274
4551
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3275
4552
  const col = unwrap(evalExpr(callArg(node.args, 1, ["column", "col"]), env)) ?? 0;
3276
4553
  const orderRaw = evalExpr(callArg(node.args, 2, ["order"]), env);
3277
- const order = typeof orderRaw === "string" && orderRaw.toLowerCase().includes("desc") ? "desc" : "asc";
3278
- m?.sort(col, order);
4554
+ const order = isDescendingOrder(orderRaw) ? "desc" : "asc";
4555
+ const sortField = sortFieldValue(evalExpr(callArg(node.args, 3, ["sort_field"]), env));
4556
+ m?.sort(col, order, sortField);
3279
4557
  return NA;
3280
4558
  }
4559
+ if (name === "matrix.sort_indices") {
4560
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
4561
+ if (!m) return NA;
4562
+ const col = unwrap(evalExpr(callArg(node.args, 1, ["column", "col"]), env)) ?? 0;
4563
+ const orderRaw = evalExpr(callArg(node.args, 2, ["order"]), env);
4564
+ const order = isDescendingOrder(orderRaw) ? "desc" : "asc";
4565
+ const sortField = sortFieldValue(evalExpr(callArg(node.args, 3, ["sort_field"]), env));
4566
+ const idx = m.sortIndices(col, order, sortField);
4567
+ const out = new PineArray();
4568
+ for (const i of idx) out.push(i);
4569
+ return out;
4570
+ }
3281
4571
  if (name === "matrix.median") {
3282
4572
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3283
4573
  return m ? m.median() : NA;
3284
4574
  }
4575
+ if (
4576
+ name === "matrix.sum_row" ||
4577
+ name === "matrix.avg_row" ||
4578
+ name === "matrix.min_row" ||
4579
+ name === "matrix.max_row" ||
4580
+ name === "matrix.mode_row"
4581
+ ) {
4582
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4583
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["row", "index"]), env));
4584
+ if (!m || i == null) return NA;
4585
+ if (name === "matrix.sum_row") return m.sumRow(i);
4586
+ if (name === "matrix.avg_row") return m.avgRow(i);
4587
+ if (name === "matrix.min_row") return m.minRow(i);
4588
+ if (name === "matrix.max_row") return m.maxRow(i);
4589
+ return m.modeRow(i) as Value;
4590
+ }
4591
+ if (
4592
+ name === "matrix.sum_col" ||
4593
+ name === "matrix.avg_col" ||
4594
+ name === "matrix.min_col" ||
4595
+ name === "matrix.max_col" ||
4596
+ name === "matrix.mode_col"
4597
+ ) {
4598
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4599
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["column", "col", "index"]), env));
4600
+ if (!m || i == null) return NA;
4601
+ if (name === "matrix.sum_col") return m.sumCol(i);
4602
+ if (name === "matrix.avg_col") return m.avgCol(i);
4603
+ if (name === "matrix.min_col") return m.minCol(i);
4604
+ if (name === "matrix.max_col") return m.maxCol(i);
4605
+ return m.modeCol(i) as Value;
4606
+ }
4607
+ if (name === "matrix.copy_row") {
4608
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4609
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["row", "index"]), env));
4610
+ if (!m || i == null) return NA;
4611
+ const arr = new PineArray();
4612
+ for (const v of m.copyRow(i)) arr.push(v);
4613
+ return arr;
4614
+ }
4615
+ if (name === "matrix.copy_col") {
4616
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4617
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["column", "col", "index"]), env));
4618
+ if (!m || i == null) return NA;
4619
+ const arr = new PineArray();
4620
+ for (const v of m.copyCol(i)) arr.push(v);
4621
+ return arr;
4622
+ }
4623
+ if (name === "matrix.fill_row") {
4624
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4625
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["row", "index"]), env));
4626
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
4627
+ if (m && i != null) m.fillRow(i, v);
4628
+ return NA;
4629
+ }
4630
+ if (name === "matrix.fill_col") {
4631
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4632
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["column", "col", "index"]), env));
4633
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
4634
+ if (m && i != null) m.fillCol(i, v);
4635
+ return NA;
4636
+ }
4637
+ if (name === "matrix.fill_diagonal") {
4638
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4639
+ const v = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4640
+ m?.fillDiagonal(v);
4641
+ return NA;
4642
+ }
4643
+ if (name === "matrix.reverse_rows") {
4644
+ asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env))?.reverseRows();
4645
+ return NA;
4646
+ }
4647
+ if (name === "matrix.reverse_cols") {
4648
+ asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env))?.reverseCols();
4649
+ return NA;
4650
+ }
4651
+ if (name === "matrix.stdev") {
4652
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4653
+ return m ? m.stdev() : NA;
4654
+ }
4655
+ if (name === "matrix.variance") {
4656
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4657
+ return m ? m.variance() : NA;
4658
+ }
4659
+ if (name === "matrix.mode_all") {
4660
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4661
+ return m ? (m.modeAll() as Value) : NA;
4662
+ }
3285
4663
  if (name === "matrix.mode") {
3286
4664
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3287
4665
  return m ? m.mode() : NA;
@@ -3301,125 +4679,808 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3301
4679
  return undefined;
3302
4680
  }
3303
4681
 
4682
+ function isChartPoint(value: Value): value is ChartPointVal {
4683
+ return typeof value === "object" && value !== null && (value as ChartPointVal).__chartPoint === true;
4684
+ }
4685
+
4686
+ function makeChartPoint(index: number | null, time: number | null, price: number | null): ChartPointVal {
4687
+ const x = index ?? time;
4688
+ return { __chartPoint: true, index, time, x, y: price, price };
4689
+ }
4690
+
4691
+ function chartPointField(obj: ChartPointVal, attr: string): Value {
4692
+ if (attr === "__chartPoint" || !Object.prototype.hasOwnProperty.call(obj, attr)) return NA;
4693
+ const v = obj[attr as keyof ChartPointVal];
4694
+ if (typeof v === "number") return Number.isFinite(v) ? v : NA;
4695
+ return NA;
4696
+ }
4697
+
4698
+ function drawingAllKind(name: string): DrawingKind | null {
4699
+ if (
4700
+ name === "line" ||
4701
+ name === "label" ||
4702
+ name === "box" ||
4703
+ name === "table" ||
4704
+ name === "polyline" ||
4705
+ name === "linefill"
4706
+ ) {
4707
+ return name;
4708
+ }
4709
+ return null;
4710
+ }
4711
+
4712
+ function drawingAllArray(env: Env, kind: DrawingKind): PineArray {
4713
+ const arr = new PineArray();
4714
+ for (const id of env.drawings.allIds(kind)) arr.push(id);
4715
+ return arr;
4716
+ }
4717
+
4718
+ function drawId(node: Call, env: Env): number | null {
4719
+ return unwrap(evalExpr(callArg(node.args, 0, ["id", "table_id", "table"]), env));
4720
+ }
4721
+
4722
+ function drawNum(node: Call, env: Env, index: number, names: string[]): number | null {
4723
+ const e = callArg(node.args, index, names);
4724
+ return e == null ? null : unwrap(evalExpr(e, env));
4725
+ }
4726
+
4727
+ function drawRaw(node: Call, env: Env, index: number, names: string[]): unknown {
4728
+ const e = callArg(node.args, index, names);
4729
+ if (e == null) return undefined;
4730
+ const v = evalExpr(e, env);
4731
+ if (typeof v === "string" || typeof v === "boolean" || isChartPoint(v) || v instanceof PineArray) return v;
4732
+ const n = unwrap(v);
4733
+ return n != null ? n : v;
4734
+ }
4735
+
4736
+ function drawText(node: Call, env: Env, index: number, names: string[], fallback = ""): string {
4737
+ const e = callArg(node.args, index, names);
4738
+ if (e == null) return fallback;
4739
+ const s = evalAsString(e, env);
4740
+ if (s != null) return s;
4741
+ const v = evalExpr(e, env);
4742
+ if (typeof v === "string") return v;
4743
+ const n = unwrap(v);
4744
+ return n == null ? fallback : String(n);
4745
+ }
4746
+
4747
+ function putFinite(extra: Record<string, unknown>, key: string, value: number | null | undefined): void {
4748
+ if (value != null && Number.isFinite(value)) extra[key] = value;
4749
+ }
4750
+
4751
+ function optionalCoord(node: Call, env: Env, index: number, names: string[], fallback: number): number | null {
4752
+ const e = callArg(node.args, index, names);
4753
+ if (e == null) return fallback;
4754
+ return unwrap(evalExpr(e, env));
4755
+ }
4756
+
4757
+ function pointXY(p: ChartPointVal): { x: number; y: number } {
4758
+ const x = p.index ?? p.time ?? p.x ?? 0;
4759
+ const y = p.price ?? p.y ?? 0;
4760
+ return { x: x ?? 0, y: y ?? 0 };
4761
+ }
4762
+
4763
+ function pointsFromValue(value: Value): unknown[] {
4764
+ const arr = asArray(value);
4765
+ if (!arr) return [];
4766
+ return arr.toValues().filter((p) => p != null);
4767
+ }
4768
+
3304
4769
  function evalDrawingCall(fname: string | null, node: Call, env: Env): Value | undefined {
4770
+ if (fname == null) return undefined;
4771
+ const book = env.drawings;
4772
+
4773
+ if (fname === "line" || fname === "box" || fname === "label" || fname === "table" || fname === "polyline" || fname === "linefill") {
4774
+ const e = callArg(node.args, 0, ["id", "x"]);
4775
+ return e == null ? NA : evalExpr(e, env);
4776
+ }
4777
+
3305
4778
  if (fname === "line.new") {
3306
- return env.drawings.lineNew(env.barIndex);
4779
+ const extra: Record<string, unknown> = {};
4780
+ putFinite(extra, "x1", optionalCoord(node, env, 0, ["x1"], 0));
4781
+ putFinite(extra, "y1", optionalCoord(node, env, 1, ["y1"], 0));
4782
+ putFinite(extra, "x2", optionalCoord(node, env, 2, ["x2"], 0));
4783
+ putFinite(extra, "y2", optionalCoord(node, env, 3, ["y2"], 0));
4784
+ const xloc = drawRaw(node, env, 4, ["xloc"]);
4785
+ if (xloc !== undefined) extra.xloc = xloc;
4786
+ const color = drawRaw(node, env, 5, ["color"]);
4787
+ if (color !== undefined) extra.color = color;
4788
+ putFinite(extra, "width", drawNum(node, env, 6, ["width"]));
4789
+ const style = drawRaw(node, env, 7, ["style"]);
4790
+ if (style !== undefined) extra.style = style;
4791
+ const extend = drawRaw(node, env, 8, ["extend"]);
4792
+ if (extend !== undefined) extra.extend = extend;
4793
+ return book.lineNew(env.barIndex, extra);
3307
4794
  }
3308
4795
  if (fname === "label.new") {
3309
- const text = evalAsString(callArg(node.args, 2, ["text"]), env);
3310
- return env.drawings.labelNew(env.barIndex, text ?? undefined);
4796
+ const pointKw = callArg(node.args, -1, ["point"]);
4797
+ const a0 = callArg(node.args, 0, ["x", "point"]);
4798
+ const v0 = pointKw != null ? evalExpr(pointKw, env) : a0 != null ? evalExpr(a0, env) : NA;
4799
+ const extra: Record<string, unknown> = {};
4800
+ let text: string;
4801
+ if (isChartPoint(v0)) {
4802
+ const xy = pointXY(v0);
4803
+ extra.x = xy.x;
4804
+ extra.y = xy.y;
4805
+ text = drawText(node, env, pointKw != null ? 0 : 1, ["text"]);
4806
+ } else {
4807
+ putFinite(extra, "x", a0 == null ? 0 : unwrap(v0));
4808
+ putFinite(extra, "y", optionalCoord(node, env, 1, ["y"], 0));
4809
+ text = drawText(node, env, 2, ["text"]);
4810
+ }
4811
+ const tooltip = drawRaw(node, env, 12, ["tooltip"]);
4812
+ if (tooltip !== undefined) extra.tooltip = tooltip;
4813
+ const style = drawRaw(node, env, 13, ["style"]);
4814
+ if (style !== undefined) extra.style = style;
4815
+ return book.labelNew(env.barIndex, text, extra);
3311
4816
  }
3312
4817
  if (fname === "box.new") {
3313
- return env.drawings.boxNew(env.barIndex);
4818
+ const extra: Record<string, unknown> = {};
4819
+ putFinite(extra, "left", optionalCoord(node, env, 0, ["left"], 0));
4820
+ putFinite(extra, "top", optionalCoord(node, env, 1, ["top"], 0));
4821
+ putFinite(extra, "right", optionalCoord(node, env, 2, ["right"], 0));
4822
+ putFinite(extra, "bottom", optionalCoord(node, env, 3, ["bottom"], 0));
4823
+ const bgcolor = drawRaw(node, env, 6, ["bgcolor"]);
4824
+ if (bgcolor !== undefined) extra.bgcolor = bgcolor;
4825
+ const text = callArg(node.args, -1, ["text"]);
4826
+ if (text != null) extra.text = drawText(node, env, -1, ["text"]);
4827
+ return book.boxNew(env.barIndex, extra);
3314
4828
  }
3315
4829
  if (fname === "alert") {
3316
4830
  const msg = evalAlertMessage(callArg(node.args, 0, ["message"]), env);
3317
- env.drawings.alert(env.barIndex, msg);
4831
+ book.alert(env.barIndex, msg);
3318
4832
  return NA;
3319
4833
  }
3320
- if (fname === "table.new") return env.drawings.tableNew(env.barIndex);
3321
- if (fname === "polyline.new") return env.drawings.polylineNew(env.barIndex);
3322
- if (fname === "linefill.new") {
3323
- const a = unwrap(evalExpr(callArg(node.args, 0, ["id1"]), env));
3324
- const b = unwrap(evalExpr(callArg(node.args, 1, ["id2"]), env));
3325
- return env.drawings.linefillNew(
3326
- env.barIndex,
3327
- a == null ? undefined : a,
3328
- b == null ? undefined : b,
3329
- );
4834
+ if (fname === "table.new") {
4835
+ const extra: Record<string, unknown> = {};
4836
+ const position = drawRaw(node, env, 0, ["position"]);
4837
+ if (position !== undefined) extra.position = position;
4838
+ putFinite(extra, "rows", drawNum(node, env, 1, ["rows"]));
4839
+ putFinite(extra, "columns", drawNum(node, env, 2, ["columns"]));
4840
+ return book.tableNew(env.barIndex, extra);
4841
+ }
4842
+ if (fname === "polyline.new") {
4843
+ const extra: Record<string, unknown> = {};
4844
+ const ptsExpr = callArg(node.args, 0, ["points"]);
4845
+ extra.points = ptsExpr == null ? [] : pointsFromValue(evalExpr(ptsExpr, env));
4846
+ const closed = drawRaw(node, env, 1, ["closed"]);
4847
+ if (closed !== undefined) extra.closed = closed;
4848
+ const color = drawRaw(node, env, 3, ["color"]);
4849
+ if (color !== undefined) extra.color = color;
4850
+ putFinite(extra, "width", drawNum(node, env, 4, ["width"]));
4851
+ return book.polylineNew(env.barIndex, extra);
4852
+ }
4853
+ if (fname === "linefill.new" || fname === "line.fill") {
4854
+ const a = unwrap(evalExpr(callArg(node.args, 0, ["id1", "line1"]), env));
4855
+ const b = unwrap(evalExpr(callArg(node.args, 1, ["id2", "line2"]), env));
4856
+ const extra: Record<string, unknown> = {};
4857
+ if (a != null) extra.id1 = a;
4858
+ if (b != null) extra.id2 = b;
4859
+ const color = drawRaw(node, env, 2, ["color"]);
4860
+ if (color !== undefined) extra.color = color;
4861
+ return book.linefillNew(env.barIndex, extra);
4862
+ }
4863
+
4864
+ if (fname.endsWith(".all")) {
4865
+ const kind = drawingAllKind(fname.slice(0, -4));
4866
+ if (kind != null) return drawingAllArray(env, kind);
4867
+ }
4868
+
4869
+ if (fname === "chart.point.new" || fname === "chart.point.from_time") {
4870
+ const time = drawNum(node, env, 0, ["time"]);
4871
+ const priceExpr = callArg(node.args, 1, ["price"]);
4872
+ const price = priceExpr == null ? 0 : unwrap(evalExpr(priceExpr, env));
4873
+ if (price == null) return NA;
4874
+ return makeChartPoint(null, time, price);
4875
+ }
4876
+ if (fname === "chart.point.from_index") {
4877
+ const index = drawNum(node, env, 0, ["index"]);
4878
+ const priceExpr = callArg(node.args, 1, ["price"]);
4879
+ const price = priceExpr == null ? 0 : unwrap(evalExpr(priceExpr, env));
4880
+ if (price == null || index == null) return NA;
4881
+ return makeChartPoint(Math.trunc(index), null, price);
4882
+ }
4883
+ if (fname === "chart.point.now") {
4884
+ const priceExpr = callArg(node.args, 0, ["price"]);
4885
+ const price = priceExpr == null ? 0 : unwrap(evalExpr(priceExpr, env));
4886
+ if (price == null) return NA;
4887
+ const time = unwrap(env.ctx.time) ?? env.barTimes[env.barIndex] ?? null;
4888
+ return makeChartPoint(env.barIndex, time, price);
4889
+ }
4890
+ if (fname === "chart.point.copy") {
4891
+ const raw = evalExpr(callArg(node.args, 0, ["point"]), env);
4892
+ if (isChartPoint(raw)) return makeChartPoint(raw.index, raw.time, raw.price);
4893
+ return makeChartPoint(null, null, 0);
4894
+ }
4895
+
4896
+ if (fname === "line.copy" || fname === "label.copy" || fname === "box.copy" || fname === "polyline.copy") {
4897
+ const id = drawId(node, env);
4898
+ if (id == null) return NA;
4899
+ const copied = book.copy(id);
4900
+ return copied == null ? NA : copied;
4901
+ }
4902
+
4903
+ if (fname === "line.delete" || fname === "label.delete" || fname === "box.delete" || fname === "table.delete" || fname === "polyline.delete" || fname === "linefill.delete") {
4904
+ const id = drawId(node, env);
4905
+ if (id != null) {
4906
+ if (fname === "line.delete") book.lineDelete(id);
4907
+ else if (fname === "label.delete") book.labelDelete(id);
4908
+ else if (fname === "box.delete") book.boxDelete(id);
4909
+ else if (fname === "table.delete") book.tableDelete(id);
4910
+ else if (fname === "polyline.delete") book.polylineDelete(id);
4911
+ else book.linefillDelete(id);
4912
+ }
4913
+ return NA;
4914
+ }
4915
+
4916
+ if (fname === "line.get_x1") {
4917
+ const id = drawId(node, env);
4918
+ return id != null ? book.lineGetX1(id) : NA;
4919
+ }
4920
+ if (fname === "line.get_y1") {
4921
+ const id = drawId(node, env);
4922
+ return id != null ? book.lineGetY1(id) : NA;
4923
+ }
4924
+ if (fname === "line.get_x2") {
4925
+ const id = drawId(node, env);
4926
+ return id != null ? book.lineGetX2(id) : NA;
4927
+ }
4928
+ if (fname === "line.get_y2") {
4929
+ const id = drawId(node, env);
4930
+ return id != null ? book.lineGetY2(id) : NA;
4931
+ }
4932
+ if (fname === "line.get_price") {
4933
+ const id = drawId(node, env);
4934
+ const x = drawNum(node, env, 1, ["x"]);
4935
+ return id != null && x != null ? book.lineGetPrice(id, x) : NA;
4936
+ }
4937
+ if (fname === "label.get_x") {
4938
+ const id = drawId(node, env);
4939
+ return id == null ? 0 : (book.labelGetX(id) ?? 0);
3330
4940
  }
4941
+ if (fname === "label.get_y") {
4942
+ const id = drawId(node, env);
4943
+ return id == null ? 0 : (book.labelGetY(id) ?? 0);
4944
+ }
4945
+ if (fname === "label.get_text") {
4946
+ const id = drawId(node, env);
4947
+ return id == null ? "" : (book.labelGetText(id) ?? "");
4948
+ }
4949
+ if (fname === "box.get_left") {
4950
+ const id = drawId(node, env);
4951
+ return id == null ? 0 : (book.boxGetLeft(id) ?? 0);
4952
+ }
4953
+ if (fname === "box.get_right") {
4954
+ const id = drawId(node, env);
4955
+ return id == null ? 0 : (book.boxGetRight(id) ?? 0);
4956
+ }
4957
+ if (fname === "box.get_top") {
4958
+ const id = drawId(node, env);
4959
+ return id == null ? 0 : (book.boxGetTop(id) ?? 0);
4960
+ }
4961
+ if (fname === "box.get_bottom") {
4962
+ const id = drawId(node, env);
4963
+ return id == null ? 0 : (book.boxGetBottom(id) ?? 0);
4964
+ }
4965
+ if (fname === "table.cell_get_text") {
4966
+ const id = drawId(node, env);
4967
+ const row = drawNum(node, env, 1, ["row"]);
4968
+ const col = drawNum(node, env, 2, ["column"]);
4969
+ if (id == null) return "";
4970
+ return book.tableCellGetText(id, col, row) ?? "";
4971
+ }
4972
+ if (fname === "linefill.get_line1") {
4973
+ const id = drawId(node, env);
4974
+ return id != null ? book.linefillGetLine1(id) : NA;
4975
+ }
4976
+ if (fname === "linefill.get_line2") {
4977
+ const id = drawId(node, env);
4978
+ return id != null ? book.linefillGetLine2(id) : NA;
4979
+ }
4980
+ if (fname === "polyline.get_points") {
4981
+ const id = drawId(node, env);
4982
+ const arr = new PineArray();
4983
+ if (id != null) for (const p of book.polylineGetPoints(id)) arr.push(p);
4984
+ return arr;
4985
+ }
4986
+
3331
4987
  if (fname === "line.set_xy") {
3332
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
4988
+ const id = drawId(node, env);
3333
4989
  if (id != null) {
3334
- env.drawings.lineSetXy(
4990
+ book.lineSetXy(
3335
4991
  id,
3336
- unwrap(evalExpr(callArg(node.args, 1, ["x1"]), env)) ?? 0,
3337
- unwrap(evalExpr(callArg(node.args, 2, ["y1"]), env)) ?? 0,
3338
- unwrap(evalExpr(callArg(node.args, 3, ["x2"]), env)) ?? 0,
3339
- unwrap(evalExpr(callArg(node.args, 4, ["y2"]), env)) ?? 0,
4992
+ drawNum(node, env, 1, ["x1"]) ?? 0,
4993
+ drawNum(node, env, 2, ["y1"]) ?? 0,
4994
+ drawNum(node, env, 3, ["x2"]) ?? 0,
4995
+ drawNum(node, env, 4, ["y2"]) ?? 0,
3340
4996
  );
3341
4997
  }
3342
4998
  return NA;
3343
4999
  }
3344
- if (fname === "label.set_text") {
3345
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3346
- if (id != null) env.drawings.labelSetText(id, evalAsString(callArg(node.args, 1, ["text"]), env) ?? "");
5000
+ if (fname === "line.set_xy1") {
5001
+ const id = drawId(node, env);
5002
+ if (id != null) book.lineSetXy1(id, drawNum(node, env, 1, ["x1", "x"]) ?? 0, drawNum(node, env, 2, ["y1", "y"]) ?? 0);
3347
5003
  return NA;
3348
5004
  }
3349
- if (fname === "line.delete" || fname === "label.delete" || fname === "box.delete") {
3350
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3351
- if (id != null) {
3352
- if (fname === "line.delete") env.drawings.lineDelete(id);
3353
- else if (fname === "label.delete") env.drawings.labelDelete(id);
3354
- else env.drawings.boxDelete(id);
3355
- }
5005
+ if (fname === "line.set_xy2") {
5006
+ const id = drawId(node, env);
5007
+ if (id != null) book.lineSetXy2(id, drawNum(node, env, 1, ["x2", "x"]) ?? 0, drawNum(node, env, 2, ["y2", "y"]) ?? 0);
5008
+ return NA;
5009
+ }
5010
+ if (fname === "line.set_x1") {
5011
+ const id = drawId(node, env);
5012
+ if (id != null) book.lineSetX1(id, drawNum(node, env, 1, ["x1", "x"]));
5013
+ return NA;
5014
+ }
5015
+ if (fname === "line.set_y1") {
5016
+ const id = drawId(node, env);
5017
+ if (id != null) book.lineSetY1(id, drawNum(node, env, 1, ["y1", "y"]));
5018
+ return NA;
5019
+ }
5020
+ if (fname === "line.set_x2") {
5021
+ const id = drawId(node, env);
5022
+ if (id != null) book.lineSetX2(id, drawNum(node, env, 1, ["x2", "x"]));
5023
+ return NA;
5024
+ }
5025
+ if (fname === "line.set_y2") {
5026
+ const id = drawId(node, env);
5027
+ if (id != null) book.lineSetY2(id, drawNum(node, env, 1, ["y2", "y"]));
3356
5028
  return NA;
3357
5029
  }
3358
5030
  if (fname === "line.set_color") {
3359
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3360
- if (id != null) env.drawings.lineSetColor(id, evalAsString(callArg(node.args, 1, ["color"]), env) ?? "");
5031
+ const id = drawId(node, env);
5032
+ if (id != null) book.lineSetColor(id, drawRaw(node, env, 1, ["color"]));
3361
5033
  return NA;
3362
5034
  }
3363
- if (fname === "line.get_price") {
3364
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3365
- const x = unwrap(evalExpr(callArg(node.args, 1, ["x"]), env));
3366
- return id != null && x != null ? env.drawings.lineGetPrice(id, x) : NA;
5035
+ if (fname === "line.set_width") {
5036
+ const id = drawId(node, env);
5037
+ if (id != null) book.lineSetWidth(id, drawRaw(node, env, 1, ["width"]));
5038
+ return NA;
3367
5039
  }
3368
- if (fname === "line.get_x1") {
3369
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3370
- return id != null ? env.drawings.getX1(id) : NA;
5040
+ if (fname === "line.set_style") {
5041
+ const id = drawId(node, env);
5042
+ if (id != null) book.lineSetStyle(id, drawRaw(node, env, 1, ["style"]));
5043
+ return NA;
3371
5044
  }
3372
- if (fname === "line.get_y1") {
3373
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3374
- return id != null ? env.drawings.getY1(id) : NA;
5045
+ if (fname === "line.set_extend") {
5046
+ const id = drawId(node, env);
5047
+ if (id != null) book.lineSetExtend(id, drawRaw(node, env, 1, ["extend"]));
5048
+ return NA;
3375
5049
  }
3376
- if (fname === "line.get_x2") {
3377
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3378
- return id != null ? env.drawings.getX2(id) : NA;
5050
+ if (fname === "line.set_xloc") {
5051
+ const id = drawId(node, env);
5052
+ if (id != null) book.lineSetXloc(id, drawRaw(node, env, 1, ["xloc"]));
5053
+ return NA;
3379
5054
  }
3380
- if (fname === "line.get_y2") {
3381
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3382
- return id != null ? env.drawings.getY2(id) : NA;
5055
+ if (fname === "line.set_first_point") {
5056
+ const id = drawId(node, env);
5057
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5058
+ if (id != null && isChartPoint(p)) {
5059
+ const xy = pointXY(p);
5060
+ book.lineSetXy1(id, xy.x, xy.y);
5061
+ }
5062
+ return NA;
3383
5063
  }
3384
- if (fname === "label.get_text") {
3385
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3386
- return id != null ? env.drawings.labelGetText(id) : NA;
5064
+ if (fname === "line.set_second_point") {
5065
+ const id = drawId(node, env);
5066
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5067
+ if (id != null && isChartPoint(p)) {
5068
+ const xy = pointXY(p);
5069
+ book.lineSetXy2(id, xy.x, xy.y);
5070
+ }
5071
+ return NA;
5072
+ }
5073
+
5074
+ if (fname === "label.set_text") {
5075
+ const id = drawId(node, env);
5076
+ if (id != null) book.labelSetText(id, drawText(node, env, 1, ["text"]));
5077
+ return NA;
3387
5078
  }
3388
5079
  if (fname === "label.set_xy") {
3389
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
5080
+ const id = drawId(node, env);
5081
+ if (id != null) book.labelSetXy(id, drawNum(node, env, 1, ["x"]) ?? 0, drawNum(node, env, 2, ["y"]) ?? 0);
5082
+ return NA;
5083
+ }
5084
+ if (fname === "label.set_x") {
5085
+ const id = drawId(node, env);
5086
+ if (id != null) book.labelSetX(id, drawNum(node, env, 1, ["x"]));
5087
+ return NA;
5088
+ }
5089
+ if (fname === "label.set_y") {
5090
+ const id = drawId(node, env);
5091
+ if (id != null) book.labelSetY(id, drawNum(node, env, 1, ["y"]));
5092
+ return NA;
5093
+ }
5094
+ if (fname === "label.set_color") {
5095
+ const id = drawId(node, env);
5096
+ if (id != null) book.labelSetColor(id, drawRaw(node, env, 1, ["color"]));
5097
+ return NA;
5098
+ }
5099
+ if (fname === "label.set_style") {
5100
+ const id = drawId(node, env);
5101
+ if (id != null) book.labelSetStyle(id, drawRaw(node, env, 1, ["style"]));
5102
+ return NA;
5103
+ }
5104
+ if (fname === "label.set_size") {
5105
+ const id = drawId(node, env);
5106
+ if (id != null) book.labelSetSize(id, drawRaw(node, env, 1, ["size"]));
5107
+ return NA;
5108
+ }
5109
+ if (fname === "label.set_tooltip") {
5110
+ const id = drawId(node, env);
5111
+ if (id != null) book.labelSetTooltip(id, drawRaw(node, env, 1, ["tooltip"]));
5112
+ return NA;
5113
+ }
5114
+ if (fname === "label.set_textalign") {
5115
+ const id = drawId(node, env);
5116
+ if (id != null) book.labelSetTextalign(id, drawRaw(node, env, 1, ["textalign", "halign"]));
5117
+ return NA;
5118
+ }
5119
+ if (fname === "label.set_textcolor") {
5120
+ const id = drawId(node, env);
5121
+ if (id != null) book.labelSetTextcolor(id, drawRaw(node, env, 1, ["color", "textcolor"]));
5122
+ return NA;
5123
+ }
5124
+ if (fname === "label.set_text_font_family") {
5125
+ const id = drawId(node, env);
5126
+ if (id != null) book.labelSetTextFontFamily(id, drawRaw(node, env, 1, ["font_family", "text_font_family"]));
5127
+ return NA;
5128
+ }
5129
+ if (fname === "label.set_text_halign") {
5130
+ const id = drawId(node, env);
5131
+ if (id != null) book.labelSetTextHalign(id, drawRaw(node, env, 1, ["halign", "text_halign"]));
5132
+ return NA;
5133
+ }
5134
+ if (fname === "label.set_text_valign") {
5135
+ const id = drawId(node, env);
5136
+ if (id != null) book.labelSetTextValign(id, drawRaw(node, env, 1, ["valign", "text_valign"]));
5137
+ return NA;
5138
+ }
5139
+ if (fname === "label.set_text_size") {
5140
+ const id = drawId(node, env);
5141
+ if (id != null) book.labelSetTextSize(id, drawRaw(node, env, 1, ["size", "text_size"]));
5142
+ return NA;
5143
+ }
5144
+ if (fname === "label.set_text_formatting") {
5145
+ const id = drawId(node, env);
5146
+ if (id != null) book.labelSetTextFormatting(id, drawRaw(node, env, 1, ["formatting", "text_formatting"]));
5147
+ return NA;
5148
+ }
5149
+ if (fname === "label.set_border_color") {
5150
+ const id = drawId(node, env);
5151
+ if (id != null) book.labelSetBorderColor(id, drawRaw(node, env, 1, ["color"]));
5152
+ return NA;
5153
+ }
5154
+ if (fname === "label.set_border_width") {
5155
+ const id = drawId(node, env);
5156
+ if (id != null) book.labelSetBorderWidth(id, drawRaw(node, env, 1, ["width"]));
5157
+ return NA;
5158
+ }
5159
+ if (fname === "label.set_border_style") {
5160
+ const id = drawId(node, env);
5161
+ if (id != null) book.labelSetBorderStyle(id, drawRaw(node, env, 1, ["style"]));
5162
+ return NA;
5163
+ }
5164
+ if (fname === "label.set_xloc") {
5165
+ const id = drawId(node, env);
5166
+ if (id != null) book.labelSetXloc(id, drawRaw(node, env, 1, ["xloc"]));
5167
+ return NA;
5168
+ }
5169
+ if (fname === "label.set_yloc") {
5170
+ const id = drawId(node, env);
5171
+ if (id != null) book.labelSetYloc(id, drawRaw(node, env, 1, ["yloc"]));
5172
+ return NA;
5173
+ }
5174
+ if (fname === "label.set_point") {
5175
+ const id = drawId(node, env);
5176
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5177
+ if (id != null && isChartPoint(p)) {
5178
+ const xy = pointXY(p);
5179
+ book.labelSetXy(id, xy.x, xy.y);
5180
+ }
5181
+ return NA;
5182
+ }
5183
+
5184
+ if (fname === "box.set_lefttop" || fname === "box.set_corners") {
5185
+ const id = drawId(node, env);
3390
5186
  if (id != null) {
3391
- env.drawings.labelSetXy(
5187
+ book.boxSetCorners(
3392
5188
  id,
3393
- unwrap(evalExpr(callArg(node.args, 1, ["x"]), env)) ?? 0,
3394
- unwrap(evalExpr(callArg(node.args, 2, ["y"]), env)) ?? 0,
5189
+ drawNum(node, env, 1, ["left", "x1"]) ?? 0,
5190
+ drawNum(node, env, 2, ["top", "y1"]) ?? 0,
5191
+ drawNum(node, env, 3, ["right", "x2"]) ?? 0,
5192
+ drawNum(node, env, 4, ["bottom", "y2"]) ?? 0,
3395
5193
  );
3396
5194
  }
3397
5195
  return NA;
3398
5196
  }
3399
- if (fname === "box.set_lefttop" || fname === "box.set_corners") {
3400
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
5197
+ if (fname === "box.set_left") {
5198
+ const id = drawId(node, env);
5199
+ if (id != null) book.boxSetLeft(id, drawNum(node, env, 1, ["left"]));
5200
+ return NA;
5201
+ }
5202
+ if (fname === "box.set_right") {
5203
+ const id = drawId(node, env);
5204
+ if (id != null) book.boxSetRight(id, drawNum(node, env, 1, ["right"]));
5205
+ return NA;
5206
+ }
5207
+ if (fname === "box.set_top") {
5208
+ const id = drawId(node, env);
5209
+ if (id != null) book.boxSetTop(id, drawNum(node, env, 1, ["top"]));
5210
+ return NA;
5211
+ }
5212
+ if (fname === "box.set_bottom") {
5213
+ const id = drawId(node, env);
5214
+ if (id != null) book.boxSetBottom(id, drawNum(node, env, 1, ["bottom"]));
5215
+ return NA;
5216
+ }
5217
+ if (fname === "box.set_bgcolor") {
5218
+ const id = drawId(node, env);
5219
+ if (id != null) book.boxSetBgcolor(id, drawRaw(node, env, 1, ["color", "bgcolor"]));
5220
+ return NA;
5221
+ }
5222
+ if (fname === "box.set_border_color") {
5223
+ const id = drawId(node, env);
5224
+ if (id != null) book.boxSetBorderColor(id, drawRaw(node, env, 1, ["color"]));
5225
+ return NA;
5226
+ }
5227
+ if (fname === "box.set_border_width") {
5228
+ const id = drawId(node, env);
5229
+ if (id != null) book.boxSetBorderWidth(id, drawRaw(node, env, 1, ["width"]));
5230
+ return NA;
5231
+ }
5232
+ if (fname === "box.set_border_style") {
5233
+ const id = drawId(node, env);
5234
+ if (id != null) book.boxSetBorderStyle(id, drawRaw(node, env, 1, ["style"]));
5235
+ return NA;
5236
+ }
5237
+ if (fname === "box.set_extend") {
5238
+ const id = drawId(node, env);
5239
+ if (id != null) book.boxSetExtend(id, drawRaw(node, env, 1, ["extend"]));
5240
+ return NA;
5241
+ }
5242
+ if (fname === "box.set_xloc") {
5243
+ const id = drawId(node, env);
5244
+ if (id != null) {
5245
+ book.boxSetLeft(id, drawNum(node, env, 1, ["left"]));
5246
+ book.boxSetRight(id, drawNum(node, env, 2, ["right"]));
5247
+ book.boxSetXloc(id, drawRaw(node, env, 3, ["xloc"]));
5248
+ }
5249
+ return NA;
5250
+ }
5251
+ if (fname === "box.set_closed") {
5252
+ const id = drawId(node, env);
5253
+ if (id != null) book.boxSetClosed(id, drawRaw(node, env, 1, ["closed"]));
5254
+ return NA;
5255
+ }
5256
+ if (fname === "box.set_rightbottom") {
5257
+ const id = drawId(node, env);
5258
+ if (id != null) book.boxSetRightBottom(id, drawNum(node, env, 1, ["right"]), drawNum(node, env, 2, ["bottom"]));
5259
+ return NA;
5260
+ }
5261
+ if (fname === "box.set_text") {
5262
+ const id = drawId(node, env);
5263
+ if (id != null) book.boxSetText(id, drawText(node, env, 1, ["text"]));
5264
+ return NA;
5265
+ }
5266
+ if (fname === "box.set_text_color") {
5267
+ const id = drawId(node, env);
5268
+ if (id != null) book.boxSetTextColor(id, drawRaw(node, env, 1, ["color", "text_color"]));
5269
+ return NA;
5270
+ }
5271
+ if (fname === "box.set_text_font_family") {
5272
+ const id = drawId(node, env);
5273
+ if (id != null) book.boxSetTextFontFamily(id, drawRaw(node, env, 1, ["font_family"]));
5274
+ return NA;
5275
+ }
5276
+ if (fname === "box.set_text_halign") {
5277
+ const id = drawId(node, env);
5278
+ if (id != null) book.boxSetTextHalign(id, drawRaw(node, env, 1, ["halign"]));
5279
+ return NA;
5280
+ }
5281
+ if (fname === "box.set_text_valign") {
5282
+ const id = drawId(node, env);
5283
+ if (id != null) book.boxSetTextValign(id, drawRaw(node, env, 1, ["valign"]));
5284
+ return NA;
5285
+ }
5286
+ if (fname === "box.set_text_size") {
5287
+ const id = drawId(node, env);
5288
+ if (id != null) book.boxSetTextSize(id, drawRaw(node, env, 1, ["size"]));
5289
+ return NA;
5290
+ }
5291
+ if (fname === "box.set_text_formatting") {
5292
+ const id = drawId(node, env);
5293
+ if (id != null) book.boxSetTextFormatting(id, drawRaw(node, env, 1, ["formatting"]));
5294
+ return NA;
5295
+ }
5296
+ if (fname === "box.set_text_wrap") {
5297
+ const id = drawId(node, env);
5298
+ if (id != null) book.boxSetTextWrap(id, drawRaw(node, env, 1, ["wrap"]));
5299
+ return NA;
5300
+ }
5301
+ if (fname === "box.set_top_left_point") {
5302
+ const id = drawId(node, env);
5303
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5304
+ if (id != null && isChartPoint(p)) {
5305
+ const xy = pointXY(p);
5306
+ book.boxSetLeftTop(id, xy.x, xy.y);
5307
+ }
5308
+ return NA;
5309
+ }
5310
+ if (fname === "box.set_bottom_right_point") {
5311
+ const id = drawId(node, env);
5312
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5313
+ if (id != null && isChartPoint(p)) {
5314
+ const xy = pointXY(p);
5315
+ book.boxSetRightBottom(id, xy.x, xy.y);
5316
+ }
5317
+ return NA;
5318
+ }
5319
+
5320
+ if (fname === "table.cell") {
5321
+ const id = drawId(node, env);
5322
+ const col = drawNum(node, env, 1, ["column"]);
5323
+ const row = drawNum(node, env, 2, ["row"]);
5324
+ const textArg = callArg(node.args, 3, ["text"]);
5325
+ const text = textArg == null ? undefined : drawText(node, env, 3, ["text"]);
5326
+ if (id != null && col != null && row != null) book.tableCell(id, col, row, text);
5327
+ return NA;
5328
+ }
5329
+ if (fname === "table.cell_set_text") {
5330
+ const id = drawId(node, env);
5331
+ const row = drawNum(node, env, 1, ["row"]);
5332
+ const col = drawNum(node, env, 2, ["column"]);
5333
+ if (id != null) book.tableCellSetText(id, col, row, drawText(node, env, 3, ["text"]));
5334
+ return NA;
5335
+ }
5336
+ if (
5337
+ fname === "table.cell_set_text_color" ||
5338
+ fname === "table.cell_set_bgcolor" ||
5339
+ fname === "table.cell_set_border_color" ||
5340
+ fname === "table.cell_set_border_width" ||
5341
+ fname === "table.cell_set_tooltip"
5342
+ ) {
5343
+ const id = drawId(node, env);
5344
+ const row = drawNum(node, env, 1, ["row"]);
5345
+ const col = drawNum(node, env, 2, ["column"]);
5346
+ const field =
5347
+ fname === "table.cell_set_text_color"
5348
+ ? "text_color"
5349
+ : fname === "table.cell_set_bgcolor"
5350
+ ? "bgcolor"
5351
+ : fname === "table.cell_set_border_color"
5352
+ ? "border_color"
5353
+ : fname === "table.cell_set_border_width"
5354
+ ? "border_width"
5355
+ : "tooltip";
5356
+ if (id != null) book.tableCellSetField(id, col, row, field, drawRaw(node, env, 3, ["color", "width", "tooltip", "text"]));
5357
+ return NA;
5358
+ }
5359
+ if (
5360
+ fname === "table.cell_set_width" ||
5361
+ fname === "table.cell_set_height" ||
5362
+ fname === "table.cell_set_text_halign" ||
5363
+ fname === "table.cell_set_text_valign" ||
5364
+ fname === "table.cell_set_text_size" ||
5365
+ fname === "table.cell_set_text_font_family" ||
5366
+ fname === "table.cell_set_text_formatting"
5367
+ ) {
5368
+ const id = drawId(node, env);
5369
+ const col = drawNum(node, env, 1, ["column", "col"]);
5370
+ const row = drawNum(node, env, 2, ["row"]);
5371
+ const field = fname.slice("table.cell_set_".length);
5372
+ if (id != null) book.tableCellSetField(id, col, row, field, drawRaw(node, env, 3, ["width", "height", "halign", "valign", "size", "font_family", "formatting"]));
5373
+ return NA;
5374
+ }
5375
+ if (fname === "table.set_position") {
5376
+ const id = drawId(node, env);
5377
+ if (id != null) book.tableSetPosition(id, drawRaw(node, env, 1, ["position"]));
5378
+ return NA;
5379
+ }
5380
+ if (fname === "table.set_bgcolor") {
5381
+ const id = drawId(node, env);
5382
+ if (id != null) book.tableSetBgcolor(id, drawRaw(node, env, 1, ["color", "bgcolor"]));
5383
+ return NA;
5384
+ }
5385
+ if (fname === "table.set_border_color") {
5386
+ const id = drawId(node, env);
5387
+ if (id != null) book.tableSetBorderColor(id, drawRaw(node, env, 1, ["color"]));
5388
+ return NA;
5389
+ }
5390
+ if (fname === "table.set_border_width") {
5391
+ const id = drawId(node, env);
5392
+ if (id != null) book.tableSetBorderWidth(id, drawRaw(node, env, 1, ["width"]));
5393
+ return NA;
5394
+ }
5395
+ if (fname === "table.set_frame_color") {
5396
+ const id = drawId(node, env);
5397
+ if (id != null) book.tableSetFrameColor(id, drawRaw(node, env, 1, ["color"]));
5398
+ return NA;
5399
+ }
5400
+ if (fname === "table.set_frame_width") {
5401
+ const id = drawId(node, env);
5402
+ if (id != null) book.tableSetFrameWidth(id, drawRaw(node, env, 1, ["width"]));
5403
+ return NA;
5404
+ }
5405
+ if (fname === "table.clear") {
5406
+ const id = drawId(node, env);
5407
+ if (id != null) {
5408
+ const a1 = callArg(node.args, 1, ["start_row"]);
5409
+ if (a1 == null) book.tableClear(id);
5410
+ else {
5411
+ book.tableClear(
5412
+ id,
5413
+ drawNum(node, env, 1, ["start_row"]),
5414
+ drawNum(node, env, 2, ["start_column", "start_col"]),
5415
+ drawNum(node, env, 3, ["end_row"]),
5416
+ drawNum(node, env, 4, ["end_column", "end_col"]),
5417
+ );
5418
+ }
5419
+ }
5420
+ return NA;
5421
+ }
5422
+ if (fname === "table.merge_cells") {
5423
+ const id = drawId(node, env);
3401
5424
  if (id != null) {
3402
- env.drawings.boxSetCorners(
5425
+ book.tableMergeCells(
3403
5426
  id,
3404
- unwrap(evalExpr(callArg(node.args, 1, ["left", "x1"]), env)) ?? 0,
3405
- unwrap(evalExpr(callArg(node.args, 2, ["top", "y1"]), env)) ?? 0,
3406
- unwrap(evalExpr(callArg(node.args, 3, ["right", "x2"]), env)) ?? 0,
3407
- unwrap(evalExpr(callArg(node.args, 4, ["bottom", "y2"]), env)) ?? 0,
5427
+ drawNum(node, env, 1, ["start_row"]),
5428
+ drawNum(node, env, 2, ["start_column", "start_col"]),
5429
+ drawNum(node, env, 3, ["end_row"]),
5430
+ drawNum(node, env, 4, ["end_column", "end_col"]),
3408
5431
  );
3409
5432
  }
3410
5433
  return NA;
3411
5434
  }
3412
- if (fname === "label.set_color") {
3413
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3414
- if (id != null) env.drawings.labelSetColor(id, evalAsString(callArg(node.args, 1, ["color"]), env) ?? "");
5435
+
5436
+ if (fname === "linefill.set_color") {
5437
+ const id = drawId(node, env);
5438
+ if (id != null) book.linefillSetColor(id, drawRaw(node, env, 1, ["color"]));
3415
5439
  return NA;
3416
5440
  }
3417
- if (fname === "table.cell") {
3418
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3419
- const col = unwrap(evalExpr(callArg(node.args, 1, ["column"]), env));
3420
- const row = unwrap(evalExpr(callArg(node.args, 2, ["row"]), env));
3421
- const text = evalAsString(callArg(node.args, 3, ["text"]), env) ?? undefined;
3422
- if (id != null && col != null && row != null) env.drawings.tableCell(id, col, row, text);
5441
+ if (fname === "polyline.set_points") {
5442
+ const id = drawId(node, env);
5443
+ if (id != null) book.polylineSetPoints(id, pointsFromValue(evalExpr(callArg(node.args, 1, ["points"]), env)));
5444
+ return NA;
5445
+ }
5446
+ if (fname === "polyline.set_line_color") {
5447
+ const id = drawId(node, env);
5448
+ if (id != null) book.polylineSetLineColor(id, drawRaw(node, env, 1, ["color"]));
5449
+ return NA;
5450
+ }
5451
+ if (fname === "polyline.set_line_width") {
5452
+ const id = drawId(node, env);
5453
+ if (id != null) book.polylineSetLineWidth(id, drawRaw(node, env, 1, ["width"]));
5454
+ return NA;
5455
+ }
5456
+ if (fname === "polyline.set_line_style") {
5457
+ const id = drawId(node, env);
5458
+ if (id != null) book.polylineSetLineStyle(id, drawRaw(node, env, 1, ["style"]));
5459
+ return NA;
5460
+ }
5461
+ if (fname === "polyline.set_fill_color") {
5462
+ const id = drawId(node, env);
5463
+ if (id != null) book.polylineSetFillColor(id, drawRaw(node, env, 1, ["fill_color", "color"]));
5464
+ return NA;
5465
+ }
5466
+ if (fname === "polyline.set_curved") {
5467
+ const id = drawId(node, env);
5468
+ if (id != null) book.polylineSetCurved(id, drawRaw(node, env, 1, ["curved"]));
5469
+ return NA;
5470
+ }
5471
+ if (fname === "polyline.set_force_overlay") {
5472
+ const id = drawId(node, env);
5473
+ if (id != null) book.polylineSetForceOverlay(id, drawRaw(node, env, 1, ["force_overlay"]));
5474
+ return NA;
5475
+ }
5476
+ if (fname === "polyline.set_closed") {
5477
+ const id = drawId(node, env);
5478
+ if (id != null) book.polylineSetClosed(id, drawRaw(node, env, 1, ["closed"]));
5479
+ return NA;
5480
+ }
5481
+ if (fname === "polyline.set_xloc") {
5482
+ const id = drawId(node, env);
5483
+ if (id != null) book.polylineSetXloc(id, drawRaw(node, env, 1, ["xloc"]));
3423
5484
  return NA;
3424
5485
  }
3425
5486
  return undefined;
@@ -3767,9 +5828,54 @@ function evalTimeCall(fname: string | null, node: Call, env: Env): Value | undef
3767
5828
  const ms = calendarMs(node, env);
3768
5829
  return ms == null ? NA : timeTradingDay(ms);
3769
5830
  }
5831
+ if (fname === "time_close") return barTimeClose(env);
5832
+ if (fname === "timenow") {
5833
+ const seeded = env.ctx.timenow;
5834
+ if (typeof seeded === "number" && Number.isFinite(seeded)) return seeded;
5835
+ if (typeof env.lastBarTime === "number" && Number.isFinite(env.lastBarTime)) return env.lastBarTime;
5836
+ const t = env.ctx.time;
5837
+ return typeof t === "number" && Number.isFinite(t) ? t : NA;
5838
+ }
3770
5839
  return undefined;
3771
5840
  }
3772
5841
 
5842
+ function barTimeClose(env: Env): number {
5843
+ const close = env.ctx.time_close;
5844
+ if (typeof close === "number" && Number.isFinite(close)) return close;
5845
+ const open = env.ctx.time;
5846
+ return typeof open === "number" && Number.isFinite(open) ? open : 0;
5847
+ }
5848
+
5849
+ function timeframeChange(node: Call, env: Env): Cell {
5850
+ const arg = callArg(node.args, 0, ["timeframe", "tf"]);
5851
+ if (arg == null) return 0;
5852
+ const raw = evalExpr(arg, env);
5853
+ const tf = typeof raw === "string" ? raw : typeof raw === "number" && Number.isFinite(raw) ? String(raw) : null;
5854
+ if (tf == null || tf.trim() === "") return 0;
5855
+ const curr = env.ctx.time;
5856
+ if (typeof curr !== "number" || !Number.isFinite(curr)) return 0;
5857
+ const hist = env.series.get("time");
5858
+ const prev = hist != null && hist.length >= 2 ? hist.get(1) : null;
5859
+ return timeframePeriodChanged(curr, prev, tf, env.barIndex) ? 1 : 0;
5860
+ }
5861
+
5862
+ /** Python `split_symbol` / `extract_prefix`: text before the first `:`, else `""`. */
5863
+ function extractPrefix(symbol: string): string {
5864
+ const s = symbol.trim();
5865
+ const i = s.indexOf(":");
5866
+ return i < 0 ? "" : s.slice(0, i);
5867
+ }
5868
+
5869
+ function syminfoPrefix(node: Call, env: Env): string {
5870
+ const arg = callArg(node.args, 0, ["tickerid", "symbol"]);
5871
+ if (arg == null) return extractPrefix(env.symbol);
5872
+ const raw = evalExpr(arg, env);
5873
+ if (raw instanceof TickerId) return extractPrefix(raw.symbol);
5874
+ if (typeof raw === "string") return extractPrefix(raw);
5875
+ if (typeof raw === "number" && Number.isFinite(raw)) return extractPrefix(String(raw));
5876
+ return "";
5877
+ }
5878
+
3773
5879
  function calendarMs(node: Call, env: Env): number | null {
3774
5880
  const arg = callArg(node.args, 0, ["time", "timestamp"]);
3775
5881
  if (arg == null) return num(env.ctx.time);