@hoox-sh/pynets 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ import type {
16
16
  Arg,
17
17
  Assign,
18
18
  Attribute,
19
+ AugAssign,
19
20
  BoolOp,
20
21
  Call,
21
22
  Conditional,
@@ -25,6 +26,7 @@ import type {
25
26
  TypeDef,
26
27
  EnumDef,
27
28
  Import,
29
+ Once,
28
30
  Param,
29
31
  ReAssign,
30
32
  Script,
@@ -59,13 +61,18 @@ import {
59
61
  import {
60
62
  timeframeFromSeconds,
61
63
  timeframeInSeconds,
64
+ timeframePeriodChanged,
62
65
  timeframeIsDaily,
66
+ timeframeIsDwm,
67
+ timeframeIsHours,
63
68
  timeframeIsIntraday,
69
+ timeframeIsMinutes,
64
70
  timeframeIsMonthly,
71
+ timeframeIsSeconds,
65
72
  timeframeIsWeekly,
66
73
  timeframeMultiplier as tfMultiplier,
67
74
  } from "./timeframe.ts";
68
- import { DrawingBook, type DrawingEvent } from "./drawings.ts";
75
+ import { DrawingBook, type DrawingEvent, type DrawingKind } from "./drawings.ts";
69
76
  import { LogBook, formatLogParts, runtimeError, type LogRecord } from "./log.ts";
70
77
  import { MemoryProvider, type BarProvider } from "./provider.ts";
71
78
  import {
@@ -138,10 +145,18 @@ import {
138
145
  strTrim,
139
146
  strUpper,
140
147
  } from "./str.ts";
141
- 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";
142
157
  import { PineMap } from "./map.ts";
143
158
  import { PineMatrix } from "./matrix.ts";
144
- import { TaEngine } from "./ta.ts";
159
+ import { TaEngine, type TaRecord } from "./ta.ts";
145
160
  import {
146
161
  CompileError,
147
162
  compileEligible,
@@ -160,17 +175,28 @@ export interface OHLCVBar {
160
175
  close?: number;
161
176
  volume?: number;
162
177
  time?: number;
178
+ time_close?: number;
163
179
  }
164
180
 
165
181
  export type InputOverrides = Record<string, number | string | boolean>;
166
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
+
167
191
  export interface RuntimeOptions {
168
192
  inputs?: InputOverrides;
169
193
  broker?: BrokerSettings;
170
194
  timeframe?: string | null;
171
- libraries?: LibraryRegistry;
195
+ libraries?: LibraryRegistry | LibrarySourceSpec[];
172
196
  /** interpret (default) | compile (JS emit) | auto (compile, fallback interpret). */
173
197
  mode?: RuntimeMode;
198
+ /** Wall-clock budget in seconds. Interpret only; checked every 32 bars. */
199
+ timeout_seconds?: number | null;
174
200
  }
175
201
 
176
202
  export interface RuntimeFill {
@@ -200,6 +226,8 @@ export interface RuntimeResult {
200
226
  plot_data?: Record<string, { data: Array<{ value: number | null; time?: number }> }>;
201
227
  /** Book-level strategy scalars when the script is a strategy (or any fills ran). */
202
228
  strategy?: StrategySummary;
229
+ /** True when interpret stopped after `timeout_seconds`. */
230
+ timed_out?: boolean;
203
231
  error?: string;
204
232
  error_kind?: string;
205
233
  }
@@ -207,8 +235,17 @@ export interface RuntimeResult {
207
235
  /** Eval-only: Cell, tagged tuple/array, color, or strategy.long/short strings. */
208
236
  type TupleVal = { __tuple: true; elts: Value[] };
209
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
+ };
210
246
  type Value =
211
247
  | Cell
248
+ | boolean // Python True/False (e.g. `barmerge.*`); coerces to 1/0 via unwrap/valuesEq
212
249
  | TupleVal
213
250
  | ArrayVal
214
251
  | Color
@@ -222,7 +259,10 @@ type Value =
222
259
  | LibraryModule
223
260
  | UdfDef
224
261
  | EnumType
225
- | EnumMember;
262
+ | EnumMember
263
+ | TaRecord
264
+ | ChartPointVal
265
+ | StrategyCashAmount;
226
266
 
227
267
  const LOOP_BREAK = Object.freeze({ __loop: "break" as const });
228
268
  const LOOP_CONTINUE = Object.freeze({ __loop: "continue" as const });
@@ -294,6 +334,11 @@ interface Env {
294
334
  barTimes: number[];
295
335
  libraries: LibraryRegistry;
296
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>;
297
342
  }
298
343
 
299
344
  export class Runtime {
@@ -302,6 +347,7 @@ export class Runtime {
302
347
  readonly timeframe: string | null;
303
348
  readonly libraries: LibraryRegistry;
304
349
  readonly mode: RuntimeMode;
350
+ readonly timeout_seconds: number | null;
305
351
 
306
352
  constructor(
307
353
  public readonly symbol = "AAPL",
@@ -310,8 +356,9 @@ export class Runtime {
310
356
  this.inputs = options?.inputs ?? {};
311
357
  this.broker = options?.broker ?? {};
312
358
  this.timeframe = options?.timeframe ?? null;
313
- this.libraries = options?.libraries ?? new LibraryRegistry();
359
+ this.libraries = resolveLibraries(options?.libraries);
314
360
  this.mode = options?.mode ?? "interpret";
361
+ this.timeout_seconds = options?.timeout_seconds ?? null;
315
362
  }
316
363
 
317
364
  /** Store Pine source for `import namespace/name/version`. */
@@ -348,7 +395,8 @@ export class Runtime {
348
395
  inputs,
349
396
  broker: extraOpts?.broker ?? this.broker,
350
397
  timeframe: extraOpts?.timeframe ?? this.timeframe,
351
- libraries: extraOpts?.libraries ?? this.libraries,
398
+ libraries: extraOpts?.libraries != null ? resolveLibraries(extraOpts.libraries) : this.libraries,
399
+ timeout_seconds: extraOpts?.timeout_seconds ?? this.timeout_seconds,
352
400
  };
353
401
  if (mode === "compile") {
354
402
  return runCompiled(source, ohlcv, host);
@@ -456,6 +504,7 @@ export function interpretTree(
456
504
  broker?: BrokerSettings;
457
505
  timeframe?: string | null;
458
506
  libraries?: LibraryRegistry;
507
+ timeout_seconds?: number | null;
459
508
  },
460
509
  ): RuntimeResult {
461
510
  const scriptNode = tree.kind === "Script" ? (tree as Script) : null;
@@ -491,12 +540,25 @@ export function interpretTree(
491
540
  barTimes: ohlcv.map((b, i) => b.time ?? i * 60_000),
492
541
  libraries: host?.libraries ?? new LibraryRegistry(),
493
542
  pendingExports: new Map(),
543
+ onceFired: new WeakSet(),
544
+ maxBarsBackDecls: [],
545
+ alertFireBars: new Map(),
494
546
  };
495
547
 
496
548
  const n = ohlcv.length;
497
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;
498
555
  try {
499
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
+ }
500
562
  beginBar(env, ohlcv[i]!, i);
501
563
  env.book.processPending(i, {
502
564
  open: num(env.ctx.open) ?? undefined,
@@ -548,7 +610,7 @@ export function interpretTree(
548
610
  series: plots,
549
611
  plots: firstTitle ? (plots[firstTitle] ?? []) : [],
550
612
  plot_meta,
551
- count: n,
613
+ count: timedOut ? barsDone : n,
552
614
  script_name,
553
615
  script_type,
554
616
  mode: "interpret",
@@ -561,6 +623,9 @@ export function interpretTree(
561
623
  ? { strategy: env.book.summary(num(env.ctx.close) ?? 0) }
562
624
  : {}),
563
625
  ...(runError ?? {}),
626
+ ...(timedOut
627
+ ? { timed_out: true, error: "Script execution timed out", error_kind: "runtime" }
628
+ : {}),
564
629
  };
565
630
  }
566
631
 
@@ -604,9 +669,24 @@ function beginBar(env: Env, bar: OHLCVBar, i: number): void {
604
669
  pushNamedSeries(env, "close", c);
605
670
  pushNamedSeries(env, "volume", vol);
606
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);
607
685
  env.ctx.bar_index = i;
608
686
  env.ctx.last_bar_index = env.barCount - 1;
609
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;
610
690
 
611
691
  bindDerived(env, "hl2", (h + l) / 2);
612
692
  bindDerived(env, "hlc3", (h + l + c) / 3);
@@ -645,17 +725,23 @@ function bindDerived(env: Env, id: string, value: Cell): void {
645
725
  }
646
726
  }
647
727
 
728
+ function unwrapSpecialize(node: expr): expr {
729
+ let cur = node;
730
+ while (cur.kind === "Specialize") cur = cur.value;
731
+ return cur;
732
+ }
733
+
648
734
  function attrPath(node: expr): string | null {
649
- if (node.kind === "Name") return node.id;
650
- if (node.kind === "Attribute") {
651
- const base = attrPath(node.value);
652
- 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;
653
740
  }
654
741
  return null;
655
742
  }
656
743
 
657
744
  function callName(node: Call): string | null {
658
- if (node.func.kind === "Name") return node.func.id;
659
745
  return attrPath(node.func);
660
746
  }
661
747
 
@@ -738,6 +824,8 @@ function callArg(
738
824
 
739
825
  function defaultPlotTitle(fname: string): string {
740
826
  if (fname === "hline") return "hline";
827
+ if (fname === "plotcandle") return "candles";
828
+ if (fname === "plotbar") return "bars";
741
829
  if (fname === "plotshape") return "shape";
742
830
  if (fname === "plotchar") return "char";
743
831
  if (fname === "plotarrow") return "arrow";
@@ -894,6 +982,61 @@ function evalAssign(stmt: Assign, env: Env): Value {
894
982
  return value;
895
983
  }
896
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
+
897
1040
  function evalReAssign(stmt: ReAssign, env: Env): Value {
898
1041
  if (stmt.target.kind === "Tuple") {
899
1042
  const packed = evalExpr(stmt.value, env);
@@ -924,6 +1067,21 @@ function applyStrategyDecl(stmt: { kind: string; value?: expr | null }, env: Env
924
1067
  if (comm != null) settings.commission = comm;
925
1068
  if (slip != null) settings.slippage = slip;
926
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;
927
1085
  if (Object.keys(settings).length) env.book.configure(settings);
928
1086
  const capital = unwrap(evalExpr(callArg(stmt.value.args, -1, ["initial_capital"]), env));
929
1087
  if (capital != null && Number.isFinite(capital)) env.book.initialCapital = capital;
@@ -978,9 +1136,12 @@ function execStmt(
978
1136
  if (stmt.kind === "ReAssign") {
979
1137
  return evalReAssign(stmt as ReAssign, env);
980
1138
  }
1139
+ if (stmt.kind === "AugAssign") {
1140
+ return evalAugAssign(stmt as AugAssign, env);
1141
+ }
981
1142
  if (stmt.kind === "Break") throw LOOP_BREAK;
982
1143
  if (stmt.kind === "Continue") throw LOOP_CONTINUE;
983
- if (stmt.kind === "Switch" || stmt.kind === "While" || stmt.kind === "If") {
1144
+ if (stmt.kind === "Switch" || stmt.kind === "While" || stmt.kind === "If" || stmt.kind === "Once") {
984
1145
  return evalExpr(stmt as unknown as expr, env);
985
1146
  }
986
1147
  if (stmt.kind !== "Expr" || !("value" in stmt) || stmt.value == null) return NA;
@@ -995,12 +1156,18 @@ function execStmt(
995
1156
  fname === "plotchar" ||
996
1157
  fname === "plotarrow" ||
997
1158
  fname === "bgcolor" ||
998
- fname === "barcolor"
1159
+ fname === "barcolor" ||
1160
+ fname === "plotcandle" ||
1161
+ fname === "plotbar"
999
1162
  ) {
1000
1163
  const plotArgs = asArgs(value.args);
1164
+ const ohlc = fname === "plotcandle" || fname === "plotbar";
1001
1165
  const title = resolvePlotTitle(plotArgs, fname);
1002
1166
  const key = uniquifyPlotKey(env, value, title);
1003
- 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);
1004
1171
  const cell = unwrap(evalExpr(seriesExpr, env));
1005
1172
  if (!env.plots[key]) {
1006
1173
  env.plots[key] = [];
@@ -1024,7 +1191,10 @@ function evalExpr(node: expr | undefined, env: Env): Value {
1024
1191
  switch (node.kind) {
1025
1192
  case "Name": {
1026
1193
  const v = ctxGet(env, node.id);
1027
- 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;
1028
1198
  }
1029
1199
  case "Constant": {
1030
1200
  const v = node.value;
@@ -1034,29 +1204,8 @@ function evalExpr(node: expr | undefined, env: Env): Value {
1034
1204
  if (v === false) return 0;
1035
1205
  return NA;
1036
1206
  }
1037
- case "BinOp": {
1038
- const leftV = evalExpr(node.left, env);
1039
- const rightV = evalExpr(node.right, env);
1040
- if (node.op.kind === "Add" && (typeof leftV === "string" || typeof rightV === "string")) {
1041
- return stringifyVal(leftV) + stringifyVal(rightV);
1042
- }
1043
- const l = unwrap(leftV);
1044
- const r = unwrap(rightV);
1045
- if (l == null || r == null) return NA;
1046
- switch (node.op.kind) {
1047
- case "Add":
1048
- return l + r;
1049
- case "Sub":
1050
- return l - r;
1051
- case "Mult":
1052
- return l * r;
1053
- case "Div":
1054
- return r === 0 ? NA : l / r;
1055
- case "Mod":
1056
- return r === 0 ? NA : l % r;
1057
- }
1058
- return NA;
1059
- }
1207
+ case "BinOp":
1208
+ return applyBinOp(node.op.kind, evalExpr(node.left, env), evalExpr(node.right, env));
1060
1209
  case "UnaryOp": {
1061
1210
  const v = unwrap(evalExpr(node.operand, env));
1062
1211
  if (node.op.kind === "Not") return isTruthy(v) ? 0 : 1;
@@ -1111,8 +1260,23 @@ function evalExpr(node: expr | undefined, env: Env): Value {
1111
1260
  return evalWhile(node, env);
1112
1261
  case "Switch":
1113
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
+ }
1114
1272
  case "BoolOp":
1115
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);
1116
1280
  }
1117
1281
  }
1118
1282
 
@@ -1189,16 +1353,27 @@ function evalSwitch(node: Switch, env: Env): Value {
1189
1353
  const subject = subjectPresent ? evalExpr(node.subject!, env) : null;
1190
1354
  for (const c of cases) {
1191
1355
  if (c.pattern == null) return execBlock(c.body, env);
1192
- if (subjectPresent) {
1193
- const pat = evalExpr(c.pattern, env);
1194
- if (valuesEq(pat, subject)) return execBlock(c.body, env);
1195
- } else if (isTruthy(unwrap(evalExpr(c.pattern, env)))) {
1196
- return execBlock(c.body, env);
1197
- }
1356
+ if (switchArmMatches(subjectPresent, subject, c.pattern, env)) return execBlock(c.body, env);
1198
1357
  }
1199
1358
  return NA;
1200
1359
  }
1201
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
+
1202
1377
  function execBlock(body: stmt[] | stmt | null | undefined, env: Env): Value {
1203
1378
  let last: Value = NA;
1204
1379
  for (const s of asStmts(body)) last = execStmt(s, env);
@@ -1253,73 +1428,154 @@ function evalForIn(node: ForIn, env: Env): Value {
1253
1428
  return last;
1254
1429
  }
1255
1430
 
1256
- /** `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. */
1257
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;
1258
1546
  if (node.value.kind === "Name") {
1259
- if (node.value.id === "strategy") {
1260
- if (node.attr === "long" || node.attr === "short") return node.attr;
1261
- if (node.attr === "position_size") return env.book.position.qty;
1262
- if (node.attr === "position_avg_price") return env.book.position.avgPrice;
1263
- if (node.attr === "netprofit") return env.book.realizedPnl;
1264
- if (node.attr === "openprofit") {
1265
- const q = env.book.position.qty;
1266
- const avg = env.book.position.avgPrice;
1267
- const mark = num(env.ctx.close);
1268
- if (q === 0 || avg == null || mark == null) return 0;
1269
- return q * (mark - avg);
1270
- }
1271
- if (node.attr === "equity") return env.book.equity(num(env.ctx.close) ?? 0);
1272
- if (node.attr === "opentrades") return env.book.opentrades;
1273
- if (node.attr === "closedtrades") return env.book.closedtrades;
1274
- if (node.attr === "wintrades") return env.book.wintrades;
1275
- if (node.attr === "losstrades") return env.book.losstrades;
1276
- if (node.attr === "eventrades") return env.book.eventrades;
1277
- if (node.attr === "grossprofit") return env.book.grossprofit;
1278
- if (node.attr === "grossloss") return env.book.grossloss;
1279
- if (node.attr === "avg_trade") return env.book.avgTrade();
1280
- if (node.attr === "avg_winning_trade") return env.book.avgWinningTrade();
1281
- if (node.attr === "avg_losing_trade") return env.book.avgLosingTrade();
1282
- if (node.attr === "netprofit_percent") {
1283
- return env.book.netprofitPercent(num(env.ctx.close) ?? 0);
1284
- }
1285
- if (node.attr === "openprofit_percent") {
1286
- return env.book.openprofitPercent(num(env.ctx.close) ?? 0);
1287
- }
1288
- if (node.attr === "grossprofit_percent") return env.book.grossprofitPercent();
1289
- if (node.attr === "grossloss_percent") return env.book.grosslossPercent();
1290
- if (node.attr === "avg_trade_percent") return env.book.avgTradePercent();
1291
- if (node.attr === "avg_winning_trade_percent") return env.book.avgWinningTradePercent();
1292
- if (node.attr === "avg_losing_trade_percent") return env.book.avgLosingTradePercent();
1293
- if (node.attr === "max_drawdown") return env.book.maxDrawdown();
1294
- if (node.attr === "max_drawdown_percent") return env.book.maxDrawdownPercent();
1295
- if (node.attr === "max_runup") return env.book.maxRunup();
1296
- if (node.attr === "max_runup_percent") return env.book.maxRunupPercent();
1297
- if (node.attr === "percent_profitable" || node.attr === "winrate") {
1298
- return env.book.percentProfitable();
1299
- }
1300
- if (node.attr === "profitfactor" || node.attr === "profit_factor") {
1301
- return env.book.profitFactor();
1302
- }
1303
- if (node.attr === "initial_capital") return env.book.initialCapital;
1304
- if (node.attr === "commission") return 0;
1305
- if (node.attr === "cash") return "cash";
1306
- if (node.attr === "fixed") return "fixed";
1307
- if (node.attr === "percent_of_equity") return "percent_of_equity";
1308
- }
1309
1547
  if (node.value.id === "syminfo") {
1310
1548
  if (node.attr === "ticker" || node.attr === "tickerid") return env.symbol;
1549
+ if (node.attr === "prefix") return extractPrefix(env.symbol);
1311
1550
  if (node.attr === "timezone") return "Etc/UTC";
1312
1551
  if (node.attr === "currency") return "USD";
1313
1552
  if (node.attr === "mintick") return 0.01;
1314
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;
1315
1557
  }
1316
1558
  if (node.value.id === "timeframe") {
1317
- if (node.attr === "period") return env.timeframe ?? "";
1318
- if (node.attr === "multiplier") return tfMultiplier(env.timeframe);
1319
- if (node.attr === "isintraday") return timeframeIsIntraday(env.timeframe) ? 1 : 0;
1320
- if (node.attr === "isdaily") return timeframeIsDaily(env.timeframe) ? 1 : 0;
1321
- if (node.attr === "isweekly") return timeframeIsWeekly(env.timeframe) ? 1 : 0;
1322
- 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;
1323
1579
  }
1324
1580
  if (node.value.id === "color") {
1325
1581
  const named = colorByName(node.attr);
@@ -1358,6 +1614,14 @@ function evalAttribute(node: Attribute, env: Env): Value {
1358
1614
  const taAttr = evalTaAttr(node.attr, env);
1359
1615
  if (taAttr !== undefined) return taAttr;
1360
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
+ }
1361
1625
  }
1362
1626
  const obj = evalExpr(node.value, env);
1363
1627
  if (obj instanceof UdtInstance) {
@@ -1377,6 +1641,33 @@ function evalAttribute(node: Attribute, env: Env): Value {
1377
1641
  if (typeof exp === "function") return NA;
1378
1642
  return exp as Value;
1379
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;
1380
1671
  return NA;
1381
1672
  }
1382
1673
 
@@ -1411,7 +1702,7 @@ function evalTaAttr(attr: string, env: Env): Value | undefined {
1411
1702
  const src = h != null && l != null && c != null ? (h + l + c) / 3 : c;
1412
1703
  return (ta.vwap as TaEngine["vwap"]).call(env.ta, site, src, num(env.ctx.volume));
1413
1704
  }
1414
- if (attr === "pvt" && typeof ta.pvt === "function") {
1705
+ if ((attr === "pvt" || attr === "vpt") && typeof ta.pvt === "function") {
1415
1706
  return (ta.pvt as TaEngine["pvt"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
1416
1707
  }
1417
1708
  if (attr === "nvi" && typeof ta.nvi === "function") {
@@ -1420,6 +1711,41 @@ function evalTaAttr(attr: string, env: Env): Value | undefined {
1420
1711
  if (attr === "pvi" && typeof ta.pvi === "function") {
1421
1712
  return (ta.pvi as TaEngine["pvi"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
1422
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
+ }
1423
1749
  return undefined;
1424
1750
  }
1425
1751
 
@@ -1455,6 +1781,9 @@ function isNaVal(value: Value): boolean {
1455
1781
  }
1456
1782
 
1457
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;
1458
1787
  if (isNaVal(left) && isNaVal(right)) return true;
1459
1788
  if (isNaVal(left) || isNaVal(right)) return false;
1460
1789
  if (left instanceof EnumMember) return left.equals(right);
@@ -1492,20 +1821,27 @@ function cmp(left: Value, op: string, right: Value): boolean | null {
1492
1821
  }
1493
1822
 
1494
1823
  function evalCall(node: Call, env: Env): Value {
1495
- if (node.func.kind === "Attribute") {
1496
- 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);
1497
1827
  if (obj instanceof LibraryModule) {
1498
- return evalLibraryMemberCall(obj, node.func.attr, node, env);
1828
+ return evalLibraryMemberCall(obj, callee.attr, node, env);
1499
1829
  }
1500
1830
  if (obj instanceof UdtInstance) {
1501
- const meth = obj.getMethod(node.func.attr);
1831
+ const meth = obj.getMethod(callee.attr);
1502
1832
  if (meth && isUdfDef(meth.body)) return evalUdf(meth.body, node, env, obj);
1503
1833
  }
1504
- if (obj instanceof UdtType && node.func.attr === "new") {
1834
+ if (obj instanceof UdtType && callee.attr === "new") {
1505
1835
  const overrides: Record<string, unknown> = {};
1836
+ let pos = 0;
1506
1837
  for (const a of asArgs(node.args)) {
1507
1838
  const key = argKeyword(a);
1508
- 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);
1509
1845
  }
1510
1846
  return obj.newInstance(overrides);
1511
1847
  }
@@ -1525,7 +1861,10 @@ function evalCall(node: Call, env: Env): Value {
1525
1861
  return env.ta.rma(site, srcArg(node, env), lenArg(node, env, 1));
1526
1862
  }
1527
1863
  if (fname === "ta.atr" || fname === "atr") {
1528
- 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;
1529
1868
  return env.ta.atr(site, num(env.ctx.high), num(env.ctx.low), num(env.ctx.close), period);
1530
1869
  }
1531
1870
  if (fname === "ta.kc" || fname === "kc") {
@@ -1542,7 +1881,12 @@ function evalCall(node: Call, env: Env): Value {
1542
1881
  const mathVal = evalMathCall(fname, node, env);
1543
1882
  if (mathVal !== undefined) return mathVal;
1544
1883
  if (isInputBuiltin(fname)) return evalInput(node, env, fname!);
1545
- if (isRequestBuiltin(fname) || fname === "request.security") {
1884
+ if (
1885
+ isRequestBuiltin(fname) ||
1886
+ fname === "request.security" ||
1887
+ fname === "security" ||
1888
+ fname === "security_lower_tf"
1889
+ ) {
1546
1890
  return evalRequestCall(fname!, node, env);
1547
1891
  }
1548
1892
  const arrVal = evalArrayCall(fname, node, env);
@@ -1595,9 +1939,44 @@ function evalCall(node: Call, env: Env): Value {
1595
1939
  }
1596
1940
  const timeVal = evalTimeCall(fname, node, env);
1597
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);
1598
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).
1599
1978
  const p = evalAsString(callArg(node.args, 0, ["timeframe", "period"]), env) ?? env.timeframe;
1600
- return timeframeInSeconds(p);
1979
+ return timeframeInSeconds(p == null || p === "" ? "D" : p);
1601
1980
  }
1602
1981
  if (fname === "timeframe.from_seconds") {
1603
1982
  const s = unwrap(evalExpr(callArg(node.args, 0, ["seconds"]), env));
@@ -1670,11 +2049,16 @@ function evalCall(node: Call, env: Env): Value {
1670
2049
  const id = evalId(callArg(node.args, 0, ["id"]));
1671
2050
  const dir = evalDirection(callArg(node.args, 1, ["direction"]), env);
1672
2051
  const qtyArg = callArg(node.args, 2, ["qty"]);
1673
- const qtyRaw = qtyArg == null ? 1 : unwrap(evalExpr(qtyArg, env));
1674
- 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
+ }
1675
2060
  const limit = unwrap(evalExpr(callArg(node.args, -1, ["limit"]), env));
1676
2061
  const stop = unwrap(evalExpr(callArg(node.args, -1, ["stop"]), env));
1677
- const mark = num(env.ctx.close);
1678
2062
  env.book.placeEntry(env.barIndex, id, dir, qty, {
1679
2063
  limit,
1680
2064
  stop,
@@ -1688,13 +2072,40 @@ function evalCall(node: Call, env: Env): Value {
1688
2072
  if (fname === "strategy.close") {
1689
2073
  const id = evalId(callArg(node.args, 0, ["id"]));
1690
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));
1691
2077
  if (env.book.position.qty !== 0 && mark != null) {
1692
- 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
+ });
1693
2082
  } else env.book.close(env.barIndex, id);
1694
2083
  return NA;
1695
2084
  }
1696
2085
  if (fname === "strategy.exit") {
1697
- 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
+ });
1698
2109
  return NA;
1699
2110
  }
1700
2111
  if (fname === "strategy.close_all") {
@@ -1726,6 +2137,8 @@ function evalCall(node: Call, env: Env): Value {
1726
2137
  }
1727
2138
  const riskVal = evalStrategyRiskCall(fname, node, env);
1728
2139
  if (riskVal !== undefined) return riskVal;
2140
+ const seriesVal = evalStrategySeriesCall(fname, node, env);
2141
+ if (seriesVal !== undefined) return seriesVal;
1729
2142
  const tradeVal = evalStrategyTradeCall(fname, node, env);
1730
2143
  if (tradeVal !== undefined) return tradeVal;
1731
2144
  if (fname != null && fname.endsWith(".new")) {
@@ -1752,7 +2165,11 @@ function evalKc(node: Call, env: Env, site: string): Value {
1752
2165
  let high = num(env.ctx.high);
1753
2166
  let low = num(env.ctx.low);
1754
2167
  let close = srcArg(node, env);
1755
- 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;
1756
2173
  let mult = numArg(node, env, 2, ["mult", "multiplier"], 1);
1757
2174
  if (positional.length >= 4) {
1758
2175
  high = unwrap(evalExpr(positional[0]!.value, env));
@@ -1869,11 +2286,72 @@ function numArg(node: Call, env: Env, index: number, names: string[], fallback:
1869
2286
 
1870
2287
  function unwrap(value: Value): Cell {
1871
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;
1872
2292
  if (typeof value === "number") return Number.isFinite(value) ? value : NA;
2293
+ if (isStrategyCashAmount(value)) return Number.isFinite(value.value) ? value.value : NA;
1873
2294
  // Objects (PineArray / Matrix / Color / tuples) are not cells — not na.
1874
2295
  return NA;
1875
2296
  }
1876
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
+
1877
2355
  function formatRunError(err: unknown): string {
1878
2356
  if (err instanceof Error) {
1879
2357
  const msg = (err.message || err.name || "runtime error").split("\n")[0]!.trim();
@@ -1892,10 +2370,27 @@ function isPlainInputs(extra: RuntimeOptions | InputOverrides): extra is InputOv
1892
2370
  !("broker" in extra) &&
1893
2371
  !("timeframe" in extra) &&
1894
2372
  !("libraries" in extra) &&
1895
- !("mode" in extra)
2373
+ !("mode" in extra) &&
2374
+ !("timeout_seconds" in extra)
1896
2375
  );
1897
2376
  }
1898
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
+
1899
2394
  function emptyCompileResult(
1900
2395
  n: number,
1901
2396
  error: string,
@@ -2028,6 +2523,7 @@ function runAuto(
2028
2523
  broker?: BrokerSettings;
2029
2524
  timeframe?: string | null;
2030
2525
  libraries?: LibraryRegistry;
2526
+ timeout_seconds?: number | null;
2031
2527
  },
2032
2528
  ): RuntimeResult {
2033
2529
  const elig = compileEligible(source);
@@ -2081,6 +2577,16 @@ function packDrawings(book: DrawingBook): DrawingEvent[] | undefined {
2081
2577
  return book.items.length === 0 ? undefined : book.items;
2082
2578
  }
2083
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
+
2084
2590
  function inputTitle(node: Call): string | null {
2085
2591
  const named = namedStringArg(node.args, "title");
2086
2592
  if (named != null) return named;
@@ -2168,6 +2674,153 @@ function evalStrategyRiskCall(fname: string | null, node: Call, env: Env): Value
2168
2674
  return undefined;
2169
2675
  }
2170
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
+
2171
2824
  function evalStrategyTradeCall(fname: string | null, node: Call, env: Env): Value | undefined {
2172
2825
  if (fname == null || !fname.startsWith("strategy.")) return undefined;
2173
2826
  const idx = unwrap(evalExpr(callArg(node.args, 0, ["trade_num", "trade_index", "index"]), env));
@@ -2176,32 +2829,54 @@ function evalStrategyTradeCall(fname: string | null, node: Call, env: Env): Valu
2176
2829
  const book = env.book;
2177
2830
  switch (fname) {
2178
2831
  case "strategy.closedtrades.entry_bar_index":
2179
- return book.closedEntryBar(i);
2832
+ return book.closedEntryBar(i) ?? 0;
2180
2833
  case "strategy.closedtrades.entry_price":
2181
- return book.closedEntryPrice(i);
2834
+ return book.closedEntryPrice(i) ?? 0;
2182
2835
  case "strategy.closedtrades.exit_bar_index":
2183
- return book.closedExitBar(i);
2836
+ return book.closedExitBar(i) ?? 0;
2184
2837
  case "strategy.closedtrades.exit_price":
2185
- return book.closedExitPrice(i);
2838
+ return book.closedExitPrice(i) ?? 0;
2186
2839
  case "strategy.closedtrades.profit":
2187
- return book.closedProfit(i);
2840
+ return book.closedProfit(i) ?? 0;
2188
2841
  case "strategy.closedtrades.size":
2189
- return book.closedSize(i);
2842
+ return book.closedSize(i) ?? 0;
2190
2843
  case "strategy.closedtrades.entry_id":
2191
2844
  case "strategy.closedtrades.exit_id":
2192
- return book.closedId(i);
2845
+ return book.closedId(i) ?? "";
2193
2846
  case "strategy.closedtrades.commission":
2194
- 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);
2195
2860
  case "strategy.opentrades.entry_bar_index":
2196
- return book.openEntryBar(i);
2861
+ return book.openEntryBar(i) ?? 0;
2197
2862
  case "strategy.opentrades.entry_price":
2198
- return book.openEntryPrice(i);
2863
+ return book.openEntryPrice(i) ?? 0;
2199
2864
  case "strategy.opentrades.size":
2200
- return book.openSize(i);
2865
+ return book.openSize(i) ?? 0;
2201
2866
  case "strategy.opentrades.entry_id":
2202
- return book.openId(i);
2867
+ return book.openId(i) ?? "";
2203
2868
  case "strategy.opentrades.profit":
2204
- 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);
2205
2880
  default:
2206
2881
  return undefined;
2207
2882
  }
@@ -2221,7 +2896,12 @@ function evalRequestSecurity(node: Call, env: Env): Cell {
2221
2896
  }
2222
2897
 
2223
2898
  function evalRequestCall(fname: string, node: Call, env: Env): Cell {
2224
- 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
+ ) {
2225
2905
  return evalRequestSecurity(node, env);
2226
2906
  }
2227
2907
  const host = { symbol: env.symbol, timeframe: env.timeframe };
@@ -2279,7 +2959,7 @@ function evalMathCall(fname: string | null, node: Call, env: Env): Cell | undefi
2279
2959
  if (fname === "math.todegrees") return mathToDegrees(cellArg(node, env, 0, ["radians", "x"]));
2280
2960
  if (fname === "math.toradians") return mathToRadians(cellArg(node, env, 0, ["degrees", "x"]));
2281
2961
  if (fname === "math.isfinite") return mathIsFinite(cellArg(node, env, 0, ["number", "x"]));
2282
- if (fname === "math.random") {
2962
+ if (fname === "math.random" || fname === "random") {
2283
2963
  const a0 = callArg(node.args, 0, ["min"]);
2284
2964
  const a1 = callArg(node.args, 1, ["max"]);
2285
2965
  return mathRandom(
@@ -2287,7 +2967,7 @@ function evalMathCall(fname: string | null, node: Call, env: Env): Cell | undefi
2287
2967
  a1 == null ? undefined : unwrap(evalExpr(a1, env)),
2288
2968
  );
2289
2969
  }
2290
- if (fname === "math.round_to_mintick") {
2970
+ if (fname === "math.round_to_mintick" || fname === "round_to_mintick") {
2291
2971
  const tick = typeof env.ctx["syminfo.mintick"] === "number" ? (env.ctx["syminfo.mintick"] as number) : 0.01;
2292
2972
  return mathRoundToMintick(cellArg(node, env, 0, ["number", "x"]), tick);
2293
2973
  }
@@ -2302,6 +2982,37 @@ function allCellArgs(node: Call, env: Env): Cell[] {
2302
2982
  return asArgs(node.args).map((a) => unwrap(evalExpr(a.value, env)));
2303
2983
  }
2304
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
+
2305
3016
  function lenOrDefault(
2306
3017
  node: Call,
2307
3018
  env: Env,
@@ -2321,10 +3032,31 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2321
3032
  const ta = env.ta as TaAny;
2322
3033
  if (fname === "ta.highest" || fname === "highest") {
2323
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
+ }
2324
3046
  return (ta.highest as TaEngine["highest"]).call(env.ta, site, srcArg(node, env), lenArg(node, env, 1));
2325
3047
  }
2326
3048
  if (fname === "ta.lowest" || fname === "lowest") {
2327
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
+ }
2328
3060
  return (ta.lowest as TaEngine["lowest"]).call(env.ta, site, srcArg(node, env), lenArg(node, env, 1));
2329
3061
  }
2330
3062
  if (fname === "ta.stdev" || fname === "stdev") {
@@ -2376,6 +3108,20 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2376
3108
  }
2377
3109
  if (fname === "ta.bb" || fname === "bb") {
2378
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
+ }
2379
3125
  const r = (ta.bb as TaEngine["bb"]).call(
2380
3126
  env.ta,
2381
3127
  site,
@@ -2409,6 +3155,29 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2409
3155
  }
2410
3156
  if (fname === "ta.vwma" || fname === "vwma") {
2411
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
+ }
2412
3181
  return (ta.vwma as TaEngine["vwma"]).call(
2413
3182
  env.ta,
2414
3183
  site,
@@ -2419,21 +3188,43 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2419
3188
  }
2420
3189
  if (fname === "ta.cci" || fname === "cci") {
2421
3190
  if (typeof ta.cci !== "function") return NA;
2422
- const h = num(env.ctx.high);
2423
- const l = num(env.ctx.low);
2424
- const c = srcArg(node, env);
2425
- const tp = h != null && l != null && c != null ? (h + l + c) / 3 : c;
2426
- 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));
2427
3216
  }
2428
3217
  if (fname === "ta.wpr" || fname === "wpr") {
2429
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.
2430
3221
  return (ta.willr as TaEngine["willr"]).call(
2431
3222
  env.ta,
2432
3223
  site,
2433
3224
  num(env.ctx.high),
2434
3225
  num(env.ctx.low),
2435
3226
  num(env.ctx.close),
2436
- lenArg(node, env, 0),
3227
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
2437
3228
  );
2438
3229
  }
2439
3230
  if (fname === "ta.willr" || fname === "willr") {
@@ -2444,28 +3235,41 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2444
3235
  num(env.ctx.high),
2445
3236
  num(env.ctx.low),
2446
3237
  num(env.ctx.close),
2447
- lenArg(node, env, 0),
3238
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
2448
3239
  );
2449
3240
  }
2450
3241
  if (fname === "ta.stoch" || fname === "stoch") {
2451
3242
  if (typeof ta.stoch !== "function") return NA;
2452
3243
  const positional = asArgs(node.args).filter((a) => argKeyword(a) == null);
2453
- const highArg = callArg(node.args, 1, ["high"]);
2454
- 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
+ }
2455
3258
  return (ta.stoch as TaEngine["stoch"]).call(
2456
3259
  env.ta,
2457
3260
  site,
2458
3261
  num(env.ctx.close),
2459
3262
  num(env.ctx.high),
2460
3263
  num(env.ctx.low),
2461
- lenArg(node, env, 0),
3264
+ length,
2462
3265
  );
2463
3266
  }
3267
+ // reference Pine form: source, high, low, length
2464
3268
  return (ta.stoch as TaEngine["stoch"]).call(
2465
3269
  env.ta,
2466
3270
  site,
2467
3271
  srcArg(node, env),
2468
- unwrap(evalExpr(highArg, env)),
3272
+ unwrap(evalExpr(callArg(node.args, 1, ["high"]), env)),
2469
3273
  unwrap(evalExpr(callArg(node.args, 2, ["low"]), env)),
2470
3274
  lenArg(node, env, 3),
2471
3275
  );
@@ -2533,6 +3337,16 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2533
3337
  }
2534
3338
  if (fname === "ta.highestbars" || fname === "highestbars") {
2535
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
+ }
2536
3350
  return (ta.highestbars as TaEngine["highestbars"]).call(
2537
3351
  env.ta,
2538
3352
  site,
@@ -2542,6 +3356,16 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2542
3356
  }
2543
3357
  if (fname === "ta.lowestbars" || fname === "lowestbars") {
2544
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
+ }
2545
3369
  return (ta.lowestbars as TaEngine["lowestbars"]).call(
2546
3370
  env.ta,
2547
3371
  site,
@@ -2598,6 +3422,20 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2598
3422
  }
2599
3423
  if (fname === "ta.pivothigh" || fname === "pivothigh") {
2600
3424
  if (typeof ta.pivothigh !== "function") return NA;
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
+ }
2601
3439
  return (ta.pivothigh as TaEngine["pivothigh"]).call(
2602
3440
  env.ta,
2603
3441
  site,
@@ -2608,6 +3446,18 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2608
3446
  }
2609
3447
  if (fname === "ta.pivotlow" || fname === "pivotlow") {
2610
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
+ }
2611
3461
  return (ta.pivotlow as TaEngine["pivotlow"]).call(
2612
3462
  env.ta,
2613
3463
  site,
@@ -2624,6 +3474,8 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2624
3474
  if (typeof ta.mfi !== "function") return NA;
2625
3475
  const positional = asArgs(node.args).filter((a) => a.name == null);
2626
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.
2627
3479
  return (ta.mfi as TaEngine["mfi"]).call(
2628
3480
  env.ta,
2629
3481
  site,
@@ -2631,7 +3483,7 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2631
3483
  num(env.ctx.low),
2632
3484
  num(env.ctx.close),
2633
3485
  num(env.ctx.volume),
2634
- lenArg(node, env, 0),
3486
+ periodLikeArg(node, env, 0, ["length"]) ?? 0,
2635
3487
  );
2636
3488
  }
2637
3489
  if (positional.length === 2) {
@@ -2682,13 +3534,44 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2682
3534
  }
2683
3535
  if (fname === "ta.adx" || fname === "adx") {
2684
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).
2685
3568
  return (ta.adx as TaEngine["adx"]).call(
2686
3569
  env.ta,
2687
3570
  site,
2688
3571
  num(env.ctx.high),
2689
3572
  num(env.ctx.low),
2690
3573
  num(env.ctx.close),
2691
- lenOrDefault(node, env, 0, ["length"], 14),
3574
+ periodLikeArg(node, env, 0, ["length"]) ?? 14,
2692
3575
  );
2693
3576
  }
2694
3577
  if (fname === "ta.correlation" || fname === "correlation") {
@@ -2719,12 +3602,15 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2719
3602
  if (typeof ta.tsi !== "function") return NA;
2720
3603
  const positional = asArgs(node.args).filter((a) => a.name == null);
2721
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.
2722
3608
  return (ta.tsi as TaEngine["tsi"]).call(
2723
3609
  env.ta,
2724
3610
  site,
2725
3611
  num(env.ctx.close),
2726
- lenOrDefault(node, env, 1, ["long", "long_length"], 25),
2727
- 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,
2728
3614
  );
2729
3615
  }
2730
3616
  return (ta.tsi as TaEngine["tsi"]).call(
@@ -2833,27 +3719,47 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2833
3719
  num(env.ctx.volume),
2834
3720
  );
2835
3721
  }
2836
- if (fname === "ta.pvt" || fname === "pvt") {
3722
+ if (fname === "ta.pvt" || fname === "pvt" || fname === "ta.vpt" || fname === "vpt") {
2837
3723
  if (typeof ta.pvt !== "function") return NA;
2838
3724
  return (ta.pvt as TaEngine["pvt"]).call(env.ta, site, num(env.ctx.close), num(env.ctx.volume));
2839
3725
  }
2840
3726
  if (fname === "ta.wad" || fname === "wad") {
2841
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
+ }
2842
3739
  return (ta.wad as TaEngine["wad"]).call(
2843
3740
  env.ta,
2844
3741
  site,
2845
3742
  num(env.ctx.high),
2846
3743
  num(env.ctx.low),
2847
3744
  num(env.ctx.close),
3745
+ num(env.ctx.volume),
2848
3746
  );
2849
3747
  }
2850
3748
  if (fname === "ta.nvi" || fname === "nvi") {
2851
3749
  if (typeof ta.nvi !== "function") return NA;
2852
- 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);
2853
3755
  }
2854
3756
  if (fname === "ta.pvi" || fname === "pvi") {
2855
3757
  if (typeof ta.pvi !== "function") return NA;
2856
- 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);
2857
3763
  }
2858
3764
  if (fname === "ta.iii" || fname === "iii") {
2859
3765
  if (typeof ta.iii !== "function") return NA;
@@ -2868,28 +3774,335 @@ function evalExtraTa(fname: string | null, node: Call, env: Env, site: string):
2868
3774
  }
2869
3775
  if (fname === "ta.wvad" || fname === "wvad") {
2870
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
+ }
2871
3789
  return (ta.wvad as TaEngine["wvad"]).call(
2872
3790
  env.ta,
2873
3791
  site,
2874
- num(env.ctx.open),
2875
3792
  num(env.ctx.high),
2876
3793
  num(env.ctx.low),
2877
3794
  num(env.ctx.close),
2878
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,
2879
3800
  );
2880
3801
  }
2881
- if (fname === "ta.pivot_point_levels") {
2882
- if (typeof ta.pivotPoints !== "function") return NA;
2883
- const r = (ta.pivotPoints as TaEngine["pivotPoints"]).call(
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(
2884
3817
  env.ta,
2885
3818
  site,
2886
3819
  num(env.ctx.high),
2887
3820
  num(env.ctx.low),
2888
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,
2889
3827
  );
2890
- return tupleOf([r.pp, r.r1, r.s1, r.r2, r.s2]);
2891
3828
  }
2892
- return undefined;
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
+ }
3869
+ if (fname === "ta.pivot_point_levels") {
3870
+ if (typeof ta.pivotPoints !== "function") return NA;
3871
+ const r = (ta.pivotPoints as TaEngine["pivotPoints"]).call(
3872
+ env.ta,
3873
+ site,
3874
+ num(env.ctx.high),
3875
+ num(env.ctx.low),
3876
+ num(env.ctx.close),
3877
+ );
3878
+ return tupleOf([r.pp, r.r1, r.s1, r.r2, r.s2]);
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
+ }
3954
+ // wave10a-start
3955
+ const wave10aArg = (index: number, names: string[], message: string): expr => {
3956
+ const arg = callArg(node.args, index, names);
3957
+ if (arg == null) runtimeError(message);
3958
+ return arg;
3959
+ };
3960
+ if (fname === "ta.hammer" || fname === "hammer") {
3961
+ const msg = "ta.hammer() requires 4 arguments: open, high, low, close";
3962
+ return env.ta.hammer(
3963
+ unwrap(evalExpr(wave10aArg(0, ["open"], msg), env)),
3964
+ unwrap(evalExpr(wave10aArg(1, ["high"], msg), env)),
3965
+ unwrap(evalExpr(wave10aArg(2, ["low"], msg), env)),
3966
+ unwrap(evalExpr(wave10aArg(3, ["close"], msg), env)),
3967
+ );
3968
+ }
3969
+ if (fname === "ta.engulfing" || fname === "engulfing") {
3970
+ const msg = "ta.engulfing() requires 4 arguments: open, high, low, close";
3971
+ unwrap(evalExpr(wave10aArg(0, ["open"], msg), env));
3972
+ unwrap(evalExpr(wave10aArg(1, ["high"], msg), env));
3973
+ unwrap(evalExpr(wave10aArg(2, ["low"], msg), env));
3974
+ unwrap(evalExpr(wave10aArg(3, ["close"], msg), env));
3975
+ return env.ta.engulfing();
3976
+ }
3977
+ if (fname === "ta.gap_detector" || fname === "gap_detector") {
3978
+ const msg = "ta.gap_detector() requires 3 arguments: high, low, prev_close";
3979
+ return env.ta.gapDetector(
3980
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env)),
3981
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env)),
3982
+ unwrap(evalExpr(wave10aArg(2, ["prev_close", "prev"], msg), env)),
3983
+ );
3984
+ }
3985
+ if (fname === "ta.double_top_bottom" || fname === "double_top_bottom") {
3986
+ const msg = "ta.double_top_bottom() requires 3 arguments";
3987
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
3988
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
3989
+ return env.ta.doubleTopBottom(unwrap(evalExpr(wave10aArg(2, ["period", "length"], msg), env)));
3990
+ }
3991
+ if (fname === "ta.inside_bar_pattern" || fname === "inside_bar_pattern") {
3992
+ const msg = "ta.inside_bar_pattern() requires 2 arguments";
3993
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env));
3994
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env));
3995
+ return env.ta.insideBarPattern();
3996
+ }
3997
+ if (fname === "ta.macd_signal" || fname === "macd_signal") {
3998
+ const msg = "ta.macd_signal() requires 2 arguments: macd_line, signal_line";
3999
+ return env.ta.macdSignal(
4000
+ unwrap(evalExpr(wave10aArg(0, ["macd_line", "macd"], msg), env)),
4001
+ unwrap(evalExpr(wave10aArg(1, ["signal_line", "signal"], msg), env)),
4002
+ );
4003
+ }
4004
+ if (fname === "ta.acceleration_factor" || fname === "acceleration_factor") {
4005
+ const msg = "ta.acceleration_factor() requires 2 arguments";
4006
+ return env.ta.accelerationFactor(
4007
+ site,
4008
+ unwrap(evalExpr(wave10aArg(0, ["momentum_list", "momentum", "source", "series"], msg), env)),
4009
+ unwrap(evalExpr(wave10aArg(1, ["period", "length"], msg), env)),
4010
+ );
4011
+ }
4012
+ if (fname === "ta.stoch_smooth" || fname === "stoch_smooth") {
4013
+ const msg = "ta.stoch_smooth() requires 6 arguments: high, low, close, period, smooth_k, smooth_d";
4014
+ return env.ta.stochSmooth(
4015
+ unwrap(evalExpr(wave10aArg(0, ["high"], msg), env)),
4016
+ unwrap(evalExpr(wave10aArg(1, ["low"], msg), env)),
4017
+ unwrap(evalExpr(wave10aArg(2, ["close"], msg), env)),
4018
+ unwrap(evalExpr(wave10aArg(3, ["period", "length"], msg), env)),
4019
+ unwrap(evalExpr(wave10aArg(4, ["smooth_k", "k"], msg), env)),
4020
+ unwrap(evalExpr(wave10aArg(5, ["smooth_d", "d"], msg), env)),
4021
+ );
4022
+ }
4023
+ // wave10a-end
4024
+ // wave10b-start
4025
+ const wave10bList = (index: number, names: string[]): Array<number | null> => {
4026
+ const raw = evalExpr(callArg(node.args, index, names), env);
4027
+ const arr = asArray(raw);
4028
+ if (arr == null) return [unwrap(raw)];
4029
+ const out: Array<number | null> = [];
4030
+ for (let i = 0; i < arr.size(); i++) out.push(arr.get(i));
4031
+ return out;
4032
+ };
4033
+ if (fname === "ta.voi" || fname === "voi") {
4034
+ return env.ta.voi(
4035
+ unwrap(evalExpr(callArg(node.args, 0, ["buy_volume", "buy"]), env)),
4036
+ unwrap(evalExpr(callArg(node.args, 1, ["sell_volume", "sell"]), env)),
4037
+ );
4038
+ }
4039
+ if (fname === "ta.bid_ask_imbalance" || fname === "bid_ask_imbalance") {
4040
+ return env.ta.bidAskImbalance(
4041
+ unwrap(evalExpr(callArg(node.args, 0, ["bid_size", "bid"]), env)),
4042
+ unwrap(evalExpr(callArg(node.args, 1, ["ask_size", "ask"]), env)),
4043
+ unwrap(evalExpr(callArg(node.args, 2, ["bid_price"]), env)),
4044
+ unwrap(evalExpr(callArg(node.args, 3, ["ask_price"]), env)),
4045
+ );
4046
+ }
4047
+ if (fname === "ta.rsi_oversold_overbought" || fname === "rsi_oversold_overbought") {
4048
+ const os = unwrap(evalExpr(callArg(node.args, 1, ["oversold_level", "oversold"]), env));
4049
+ const ob = unwrap(evalExpr(callArg(node.args, 2, ["overbought_level", "overbought"]), env));
4050
+ return env.ta.rsiOversoldOverbought(
4051
+ unwrap(evalExpr(callArg(node.args, 0, ["rsi_series", "rsi", "source"]), env)),
4052
+ os ?? Number.NaN,
4053
+ ob ?? Number.NaN,
4054
+ );
4055
+ }
4056
+ if (fname === "ta.rsi_divergence" || fname === "rsi_divergence") {
4057
+ const period = unwrap(evalExpr(callArg(node.args, 1, ["period", "length"]), env));
4058
+ return env.ta.rsiDivergence(wave10bList(0, ["rsi_series", "series", "source"]), period ?? 0);
4059
+ }
4060
+ if (fname === "ta.cumulative_delta" || fname === "cumulative_delta") {
4061
+ const period = unwrap(evalExpr(callArg(node.args, 2, ["period", "length"]), env));
4062
+ return env.ta.cumulativeDelta(
4063
+ site,
4064
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
4065
+ unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env)),
4066
+ period ?? 0,
4067
+ );
4068
+ }
4069
+ if (fname === "ta.volume_momentum" || fname === "volume_momentum") {
4070
+ const period = unwrap(evalExpr(callArg(node.args, 1, ["period", "length"]), env));
4071
+ return env.ta.volumeMomentum(
4072
+ site,
4073
+ unwrap(evalExpr(callArg(node.args, 0, ["volume", "source"]), env)),
4074
+ period ?? 0,
4075
+ );
4076
+ }
4077
+ if (fname === "ta.volume_profile_high" || fname === "volume_profile_high") {
4078
+ return env.ta.volumeProfileHigh(
4079
+ wave10bList(0, ["close", "price", "source", "series"]),
4080
+ wave10bList(1, ["volume"]),
4081
+ );
4082
+ }
4083
+ if (fname === "ta.volume_profile_low" || fname === "volume_profile_low") {
4084
+ const period = unwrap(evalExpr(callArg(node.args, 2, ["period", "length"]), env));
4085
+ const levels = unwrap(evalExpr(callArg(node.args, 3, ["levels"]), env));
4086
+ return env.ta.volumeProfileLow(
4087
+ site,
4088
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
4089
+ unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env)),
4090
+ period ?? 0,
4091
+ levels ?? 0,
4092
+ );
4093
+ }
4094
+ if (fname === "ta.volume_thrust" || fname === "volume_thrust") {
4095
+ const sens = unwrap(evalExpr(callArg(node.args, 3, ["sensitivity"]), env));
4096
+ return env.ta.volumeThrust(
4097
+ site,
4098
+ unwrap(evalExpr(callArg(node.args, 0, ["close", "source"]), env)),
4099
+ unwrap(evalExpr(callArg(node.args, 1, ["volume"]), env)),
4100
+ unwrap(evalExpr(callArg(node.args, 2, ["volume_sma", "vol_sma", "sma"]), env)),
4101
+ sens ?? 0.3,
4102
+ );
4103
+ }
4104
+ // wave10b-end
4105
+ return undefined;
2893
4106
  }
2894
4107
 
2895
4108
  function hlc3(env: Env): Cell {
@@ -2905,37 +4118,74 @@ function asArray(value: Value): PineArray | null {
2905
4118
  if (value && typeof value === "object" && "__array" in value) {
2906
4119
  const elts = (value as ArrayVal).elts;
2907
4120
  const arr = new PineArray();
2908
- for (const e of elts) arr.push(unwrap(e));
4121
+ for (const e of elts) arr.push(storeValue(e));
2909
4122
  return arr;
2910
4123
  }
2911
4124
  return null;
2912
4125
  }
2913
4126
 
4127
+ /** Every `array.new*` alias Python registers in `_array_builtin_map`. */
4128
+ const ARRAY_NEW_FNS = new Set([
4129
+ "array.new",
4130
+ "array.new_bool",
4131
+ "array.new_int",
4132
+ "array.new_float",
4133
+ "array.new_string",
4134
+ "array.new_color",
4135
+ "array.newbool",
4136
+ "array.newint",
4137
+ "array.newfloat",
4138
+ "array.newstring",
4139
+ "array.newcolor",
4140
+ "array.newlabel",
4141
+ "array.newline",
4142
+ "array.newbox",
4143
+ "array.newtable",
4144
+ "array.newpolyline",
4145
+ "array.newlinefill",
4146
+ "array.new_label",
4147
+ "array.new_line",
4148
+ "array.new_box",
4149
+ "array.new_table",
4150
+ "array.new_polyline",
4151
+ "array.new_linefill",
4152
+ "array.new_chart.point",
4153
+ ]);
4154
+
4155
+ function isArrayNew(fname: string | null): boolean {
4156
+ const name = arrayBuiltin(fname);
4157
+ return name != null && ARRAY_NEW_FNS.has(name);
4158
+ }
4159
+
4160
+ function arrayBuiltin(fname: string | null): string | null {
4161
+ if (fname == null || !fname.startsWith("array.")) return null;
4162
+ const lt = fname.indexOf("<");
4163
+ return lt >= 0 ? fname.slice(0, lt) : fname;
4164
+ }
4165
+
2914
4166
  function evalArrayCall(fname: string | null, node: Call, env: Env): Value | undefined {
2915
- if (
2916
- fname === "array.new_float" ||
2917
- fname === "array.new" ||
2918
- fname === "array.new_int"
2919
- ) {
4167
+ const aname = arrayBuiltin(fname);
4168
+ if (isArrayNew(fname)) {
2920
4169
  const size = unwrap(evalExpr(callArg(node.args, 0, ["size"]), env)) ?? 0;
2921
- const initial = unwrap(evalExpr(callArg(node.args, 1, ["initial_value"]), env));
4170
+ const initialArg = callArg(node.args, 1, ["initial_value"]);
4171
+ const initial = initialArg == null ? undefined : storeValue(evalExpr(initialArg, env));
2922
4172
  return new PineArray(size, initial);
2923
4173
  }
2924
- if (fname === "array.push") {
4174
+ if (aname === "array.push") {
2925
4175
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2926
- const v = unwrap(evalExpr(callArg(node.args, 1, ["value"]), env));
4176
+ const v = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
2927
4177
  arr?.push(v);
2928
4178
  return NA;
2929
4179
  }
2930
- if (fname === "array.get") {
4180
+ if (aname === "array.get") {
2931
4181
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2932
4182
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2933
- return arr ? arr.get(i ?? 0) : NA;
4183
+ return arr ? asArrayValue(arr.get(i ?? 0)) : NA;
2934
4184
  }
2935
- if (fname === "array.set") {
4185
+ if (aname === "array.set") {
2936
4186
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2937
4187
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2938
- const v = unwrap(evalExpr(callArg(node.args, 2, ["value"]), env));
4188
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
2939
4189
  if (arr && i != null) arr.set(i, v);
2940
4190
  return NA;
2941
4191
  }
@@ -2945,7 +4195,7 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2945
4195
  }
2946
4196
  if (fname === "array.pop") {
2947
4197
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2948
- return arr ? arr.pop() : NA;
4198
+ return arr ? asArrayValue(arr.pop()) : NA;
2949
4199
  }
2950
4200
  if (fname === "array.clear") {
2951
4201
  asArray(evalExpr(callArg(node.args, 0, ["id"]), env))?.clear();
@@ -2953,40 +4203,40 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
2953
4203
  }
2954
4204
  if (fname === "array.unshift") {
2955
4205
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2956
- arr?.unshift(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env)));
4206
+ arr?.unshift(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env)));
2957
4207
  return NA;
2958
4208
  }
2959
4209
  if (fname === "array.shift") {
2960
4210
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2961
- return arr ? arr.shift() : NA;
4211
+ return arr ? asArrayValue(arr.shift()) : NA;
2962
4212
  }
2963
4213
  if (fname === "array.includes") {
2964
4214
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2965
- return arr?.includes(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) ? 1 : 0;
4215
+ return arr?.includes(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env))) ? 1 : 0;
2966
4216
  }
2967
4217
  if (fname === "array.first") {
2968
4218
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2969
- return arr ? arr.first() : NA;
4219
+ return arr ? asArrayValue(arr.first()) : NA;
2970
4220
  }
2971
4221
  if (fname === "array.last") {
2972
4222
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2973
- return arr ? arr.last() : NA;
4223
+ return arr ? asArrayValue(arr.last()) : NA;
2974
4224
  }
2975
4225
  if (fname === "array.insert") {
2976
4226
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2977
4227
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2978
- const v = unwrap(evalExpr(callArg(node.args, 2, ["value"]), env));
4228
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
2979
4229
  if (arr && i != null) arr.insert(i, v);
2980
4230
  return NA;
2981
4231
  }
2982
4232
  if (fname === "array.remove") {
2983
4233
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2984
4234
  const i = unwrap(evalExpr(callArg(node.args, 1, ["index"]), env));
2985
- return arr && i != null ? arr.remove(i) : NA;
4235
+ return arr && i != null ? asArrayValue(arr.remove(i)) : NA;
2986
4236
  }
2987
4237
  if (fname === "array.fill") {
2988
4238
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
2989
- arr?.fill(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env)));
4239
+ arr?.fill(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env)));
2990
4240
  return NA;
2991
4241
  }
2992
4242
  if (fname === "array.slice") {
@@ -3006,14 +4256,13 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
3006
4256
  }
3007
4257
  if (fname === "array.sort") {
3008
4258
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3009
- const orderRaw = evalExpr(callArg(node.args, 1, ["order"]), env);
3010
- const order = typeof orderRaw === "string" && orderRaw.toLowerCase().includes("desc") ? "desc" : "asc";
3011
- arr?.sort(order);
4259
+ const parsed = parseSortArgs(node, env);
4260
+ arr?.sort(parsed.order, parsed.sortField);
3012
4261
  return NA;
3013
4262
  }
3014
4263
  if (fname === "array.indexof") {
3015
4264
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3016
- return arr ? arr.indexof(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4265
+ return arr ? arr.indexof(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
3017
4266
  }
3018
4267
  if (fname === "array.avg") {
3019
4268
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
@@ -3036,19 +4285,14 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
3036
4285
  const sep = evalAsString(callArg(node.args, 1, ["separator", "sep"]), env);
3037
4286
  return arr ? arr.join(sep ?? ",") : NA;
3038
4287
  }
3039
- if (fname === "array.new_bool" || fname === "array.new_string") {
3040
- const size = unwrap(evalExpr(callArg(node.args, 0, ["size"]), env)) ?? 0;
3041
- const initial = unwrap(evalExpr(callArg(node.args, 1, ["initial_value"]), env));
3042
- return new PineArray(size, initial);
3043
- }
3044
4288
  if (fname === "array.from") {
3045
4289
  const arr = new PineArray();
3046
- for (const a of asArgs(node.args)) arr.push(unwrap(evalExpr(a.value, env)));
4290
+ for (const a of asArgs(node.args)) arr.push(storeValue(evalExpr(a.value, env)));
3047
4291
  return arr;
3048
4292
  }
3049
4293
  if (fname === "array.lastindexof") {
3050
4294
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3051
- return arr ? arr.lastIndexOf(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4295
+ return arr ? arr.lastIndexOf(storeValue(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
3052
4296
  }
3053
4297
  if (fname === "array.concat") {
3054
4298
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
@@ -3081,15 +4325,18 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
3081
4325
  }
3082
4326
  if (fname === "array.binary_search") {
3083
4327
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3084
- return arr ? arr.binarySearch(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4328
+ const value = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4329
+ return arr ? arr.binarySearch(value, evalSortField(node, env)) : NA;
3085
4330
  }
3086
4331
  if (fname === "array.binary_search_leftmost") {
3087
4332
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3088
- return arr ? arr.binarySearchLeftmost(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4333
+ const value = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4334
+ return arr ? arr.binarySearchLeftmost(value, evalSortField(node, env)) : NA;
3089
4335
  }
3090
4336
  if (fname === "array.binary_search_rightmost") {
3091
4337
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3092
- return arr ? arr.binarySearchRightmost(unwrap(evalExpr(callArg(node.args, 1, ["value"]), env))) : NA;
4338
+ const value = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4339
+ return arr ? arr.binarySearchRightmost(value, evalSortField(node, env)) : NA;
3093
4340
  }
3094
4341
  if (fname === "array.stdev") {
3095
4342
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
@@ -3125,10 +4372,9 @@ function evalArrayCall(fname: string | null, node: Call, env: Env): Value | unde
3125
4372
  }
3126
4373
  if (fname === "array.sort_indices") {
3127
4374
  const arr = asArray(evalExpr(callArg(node.args, 0, ["id"]), env));
3128
- const orderRaw = evalExpr(callArg(node.args, 1, ["order"]), env);
3129
- const order = typeof orderRaw === "string" && orderRaw.toLowerCase().includes("desc") ? "desc" : "asc";
4375
+ const parsed = parseSortArgs(node, env);
3130
4376
  if (!arr) return NA;
3131
- const idx = arr.sortIndices(order);
4377
+ const idx = arr.sortIndices(parsed.order, parsed.sortField);
3132
4378
  if (idx == null) return NA;
3133
4379
  const out = new PineArray();
3134
4380
  for (const i of idx) out.push(i);
@@ -3230,20 +4476,20 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3230
4476
  const rows = unwrap(evalExpr(callArg(node.args, 0, ["rows"]), env)) ?? 0;
3231
4477
  const cols = unwrap(evalExpr(callArg(node.args, 1, ["columns", "cols"]), env)) ?? 0;
3232
4478
  const initialArg = callArg(node.args, 2, ["initial_value", "initial"]);
3233
- const initial = initialArg == null ? undefined : unwrap(evalExpr(initialArg, env));
4479
+ const initial = initialArg == null ? undefined : storeValue(evalExpr(initialArg, env));
3234
4480
  return new PineMatrix(rows, cols, initial);
3235
4481
  }
3236
4482
  if (name === "matrix.get") {
3237
4483
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3238
4484
  const row = unwrap(evalExpr(callArg(node.args, 1, ["row"]), env));
3239
4485
  const col = unwrap(evalExpr(callArg(node.args, 2, ["column", "col"]), env));
3240
- return m ? m.get(row ?? 0, col ?? 0) : NA;
4486
+ return m ? asArrayValue(m.get(row ?? 0, col ?? 0)) : NA;
3241
4487
  }
3242
4488
  if (name === "matrix.set") {
3243
4489
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3244
4490
  const row = unwrap(evalExpr(callArg(node.args, 1, ["row"]), env));
3245
4491
  const col = unwrap(evalExpr(callArg(node.args, 2, ["column", "col"]), env));
3246
- const v = unwrap(evalExpr(callArg(node.args, 3, ["value"]), env));
4492
+ const v = storeValue(evalExpr(callArg(node.args, 3, ["value"]), env));
3247
4493
  if (m && row != null && col != null) m.set(row, col, v);
3248
4494
  return NA;
3249
4495
  }
@@ -3257,7 +4503,7 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3257
4503
  }
3258
4504
  if (name === "matrix.fill") {
3259
4505
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3260
- const v = unwrap(evalExpr(callArg(node.args, 1, ["value"]), env));
4506
+ const v = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
3261
4507
  m?.fill(v);
3262
4508
  return NA;
3263
4509
  }
@@ -3456,14 +4702,115 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3456
4702
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3457
4703
  const col = unwrap(evalExpr(callArg(node.args, 1, ["column", "col"]), env)) ?? 0;
3458
4704
  const orderRaw = evalExpr(callArg(node.args, 2, ["order"]), env);
3459
- const order = typeof orderRaw === "string" && orderRaw.toLowerCase().includes("desc") ? "desc" : "asc";
3460
- m?.sort(col, order);
4705
+ const order = isDescendingOrder(orderRaw) ? "desc" : "asc";
4706
+ const sortField = sortFieldValue(evalExpr(callArg(node.args, 3, ["sort_field"]), env));
4707
+ m?.sort(col, order, sortField);
3461
4708
  return NA;
3462
4709
  }
4710
+ if (name === "matrix.sort_indices") {
4711
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
4712
+ if (!m) return NA;
4713
+ const col = unwrap(evalExpr(callArg(node.args, 1, ["column", "col"]), env)) ?? 0;
4714
+ const orderRaw = evalExpr(callArg(node.args, 2, ["order"]), env);
4715
+ const order = isDescendingOrder(orderRaw) ? "desc" : "asc";
4716
+ const sortField = sortFieldValue(evalExpr(callArg(node.args, 3, ["sort_field"]), env));
4717
+ const idx = m.sortIndices(col, order, sortField);
4718
+ const out = new PineArray();
4719
+ for (const i of idx) out.push(i);
4720
+ return out;
4721
+ }
3463
4722
  if (name === "matrix.median") {
3464
4723
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3465
4724
  return m ? m.median() : NA;
3466
4725
  }
4726
+ if (
4727
+ name === "matrix.sum_row" ||
4728
+ name === "matrix.avg_row" ||
4729
+ name === "matrix.min_row" ||
4730
+ name === "matrix.max_row" ||
4731
+ name === "matrix.mode_row"
4732
+ ) {
4733
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4734
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["row", "index"]), env));
4735
+ if (!m || i == null) return NA;
4736
+ if (name === "matrix.sum_row") return m.sumRow(i);
4737
+ if (name === "matrix.avg_row") return m.avgRow(i);
4738
+ if (name === "matrix.min_row") return m.minRow(i);
4739
+ if (name === "matrix.max_row") return m.maxRow(i);
4740
+ return m.modeRow(i) as Value;
4741
+ }
4742
+ if (
4743
+ name === "matrix.sum_col" ||
4744
+ name === "matrix.avg_col" ||
4745
+ name === "matrix.min_col" ||
4746
+ name === "matrix.max_col" ||
4747
+ name === "matrix.mode_col"
4748
+ ) {
4749
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4750
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["column", "col", "index"]), env));
4751
+ if (!m || i == null) return NA;
4752
+ if (name === "matrix.sum_col") return m.sumCol(i);
4753
+ if (name === "matrix.avg_col") return m.avgCol(i);
4754
+ if (name === "matrix.min_col") return m.minCol(i);
4755
+ if (name === "matrix.max_col") return m.maxCol(i);
4756
+ return m.modeCol(i) as Value;
4757
+ }
4758
+ if (name === "matrix.copy_row") {
4759
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4760
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["row", "index"]), env));
4761
+ if (!m || i == null) return NA;
4762
+ const arr = new PineArray();
4763
+ for (const v of m.copyRow(i)) arr.push(v);
4764
+ return arr;
4765
+ }
4766
+ if (name === "matrix.copy_col") {
4767
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4768
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["column", "col", "index"]), env));
4769
+ if (!m || i == null) return NA;
4770
+ const arr = new PineArray();
4771
+ for (const v of m.copyCol(i)) arr.push(v);
4772
+ return arr;
4773
+ }
4774
+ if (name === "matrix.fill_row") {
4775
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4776
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["row", "index"]), env));
4777
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
4778
+ if (m && i != null) m.fillRow(i, v);
4779
+ return NA;
4780
+ }
4781
+ if (name === "matrix.fill_col") {
4782
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4783
+ const i = unwrap(evalExpr(callArg(node.args, 1, ["column", "col", "index"]), env));
4784
+ const v = storeValue(evalExpr(callArg(node.args, 2, ["value"]), env));
4785
+ if (m && i != null) m.fillCol(i, v);
4786
+ return NA;
4787
+ }
4788
+ if (name === "matrix.fill_diagonal") {
4789
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4790
+ const v = storeValue(evalExpr(callArg(node.args, 1, ["value"]), env));
4791
+ m?.fillDiagonal(v);
4792
+ return NA;
4793
+ }
4794
+ if (name === "matrix.reverse_rows") {
4795
+ asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env))?.reverseRows();
4796
+ return NA;
4797
+ }
4798
+ if (name === "matrix.reverse_cols") {
4799
+ asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env))?.reverseCols();
4800
+ return NA;
4801
+ }
4802
+ if (name === "matrix.stdev") {
4803
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4804
+ return m ? m.stdev() : NA;
4805
+ }
4806
+ if (name === "matrix.variance") {
4807
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4808
+ return m ? m.variance() : NA;
4809
+ }
4810
+ if (name === "matrix.mode_all") {
4811
+ const m = asMatrix(evalExpr(callArg(node.args, 0, ["id", "matrix"]), env));
4812
+ return m ? (m.modeAll() as Value) : NA;
4813
+ }
3467
4814
  if (name === "matrix.mode") {
3468
4815
  const m = asMatrix(evalExpr(callArg(node.args, 0, ["id"]), env));
3469
4816
  return m ? m.mode() : NA;
@@ -3483,125 +4830,808 @@ function evalMatrixCall(fname: string | null, node: Call, env: Env): Value | und
3483
4830
  return undefined;
3484
4831
  }
3485
4832
 
4833
+ function isChartPoint(value: Value): value is ChartPointVal {
4834
+ return typeof value === "object" && value !== null && (value as ChartPointVal).__chartPoint === true;
4835
+ }
4836
+
4837
+ function makeChartPoint(index: number | null, time: number | null, price: number | null): ChartPointVal {
4838
+ const x = index ?? time;
4839
+ return { __chartPoint: true, index, time, x, y: price, price };
4840
+ }
4841
+
4842
+ function chartPointField(obj: ChartPointVal, attr: string): Value {
4843
+ if (attr === "__chartPoint" || !Object.prototype.hasOwnProperty.call(obj, attr)) return NA;
4844
+ const v = obj[attr as keyof ChartPointVal];
4845
+ if (typeof v === "number") return Number.isFinite(v) ? v : NA;
4846
+ return NA;
4847
+ }
4848
+
4849
+ function drawingAllKind(name: string): DrawingKind | null {
4850
+ if (
4851
+ name === "line" ||
4852
+ name === "label" ||
4853
+ name === "box" ||
4854
+ name === "table" ||
4855
+ name === "polyline" ||
4856
+ name === "linefill"
4857
+ ) {
4858
+ return name;
4859
+ }
4860
+ return null;
4861
+ }
4862
+
4863
+ function drawingAllArray(env: Env, kind: DrawingKind): PineArray {
4864
+ const arr = new PineArray();
4865
+ for (const id of env.drawings.allIds(kind)) arr.push(id);
4866
+ return arr;
4867
+ }
4868
+
4869
+ function drawId(node: Call, env: Env): number | null {
4870
+ return unwrap(evalExpr(callArg(node.args, 0, ["id", "table_id", "table"]), env));
4871
+ }
4872
+
4873
+ function drawNum(node: Call, env: Env, index: number, names: string[]): number | null {
4874
+ const e = callArg(node.args, index, names);
4875
+ return e == null ? null : unwrap(evalExpr(e, env));
4876
+ }
4877
+
4878
+ function drawRaw(node: Call, env: Env, index: number, names: string[]): unknown {
4879
+ const e = callArg(node.args, index, names);
4880
+ if (e == null) return undefined;
4881
+ const v = evalExpr(e, env);
4882
+ if (typeof v === "string" || typeof v === "boolean" || isChartPoint(v) || v instanceof PineArray) return v;
4883
+ const n = unwrap(v);
4884
+ return n != null ? n : v;
4885
+ }
4886
+
4887
+ function drawText(node: Call, env: Env, index: number, names: string[], fallback = ""): string {
4888
+ const e = callArg(node.args, index, names);
4889
+ if (e == null) return fallback;
4890
+ const s = evalAsString(e, env);
4891
+ if (s != null) return s;
4892
+ const v = evalExpr(e, env);
4893
+ if (typeof v === "string") return v;
4894
+ const n = unwrap(v);
4895
+ return n == null ? fallback : String(n);
4896
+ }
4897
+
4898
+ function putFinite(extra: Record<string, unknown>, key: string, value: number | null | undefined): void {
4899
+ if (value != null && Number.isFinite(value)) extra[key] = value;
4900
+ }
4901
+
4902
+ function optionalCoord(node: Call, env: Env, index: number, names: string[], fallback: number): number | null {
4903
+ const e = callArg(node.args, index, names);
4904
+ if (e == null) return fallback;
4905
+ return unwrap(evalExpr(e, env));
4906
+ }
4907
+
4908
+ function pointXY(p: ChartPointVal): { x: number; y: number } {
4909
+ const x = p.index ?? p.time ?? p.x ?? 0;
4910
+ const y = p.price ?? p.y ?? 0;
4911
+ return { x: x ?? 0, y: y ?? 0 };
4912
+ }
4913
+
4914
+ function pointsFromValue(value: Value): unknown[] {
4915
+ const arr = asArray(value);
4916
+ if (!arr) return [];
4917
+ return arr.toValues().filter((p) => p != null);
4918
+ }
4919
+
3486
4920
  function evalDrawingCall(fname: string | null, node: Call, env: Env): Value | undefined {
4921
+ if (fname == null) return undefined;
4922
+ const book = env.drawings;
4923
+
4924
+ if (fname === "line" || fname === "box" || fname === "label" || fname === "table" || fname === "polyline" || fname === "linefill") {
4925
+ const e = callArg(node.args, 0, ["id", "x"]);
4926
+ return e == null ? NA : evalExpr(e, env);
4927
+ }
4928
+
3487
4929
  if (fname === "line.new") {
3488
- return env.drawings.lineNew(env.barIndex);
4930
+ const extra: Record<string, unknown> = {};
4931
+ putFinite(extra, "x1", optionalCoord(node, env, 0, ["x1"], 0));
4932
+ putFinite(extra, "y1", optionalCoord(node, env, 1, ["y1"], 0));
4933
+ putFinite(extra, "x2", optionalCoord(node, env, 2, ["x2"], 0));
4934
+ putFinite(extra, "y2", optionalCoord(node, env, 3, ["y2"], 0));
4935
+ const xloc = drawRaw(node, env, 4, ["xloc"]);
4936
+ if (xloc !== undefined) extra.xloc = xloc;
4937
+ const color = drawRaw(node, env, 5, ["color"]);
4938
+ if (color !== undefined) extra.color = color;
4939
+ putFinite(extra, "width", drawNum(node, env, 6, ["width"]));
4940
+ const style = drawRaw(node, env, 7, ["style"]);
4941
+ if (style !== undefined) extra.style = style;
4942
+ const extend = drawRaw(node, env, 8, ["extend"]);
4943
+ if (extend !== undefined) extra.extend = extend;
4944
+ return book.lineNew(env.barIndex, extra);
3489
4945
  }
3490
4946
  if (fname === "label.new") {
3491
- const text = evalAsString(callArg(node.args, 2, ["text"]), env);
3492
- return env.drawings.labelNew(env.barIndex, text ?? undefined);
4947
+ const pointKw = callArg(node.args, -1, ["point"]);
4948
+ const a0 = callArg(node.args, 0, ["x", "point"]);
4949
+ const v0 = pointKw != null ? evalExpr(pointKw, env) : a0 != null ? evalExpr(a0, env) : NA;
4950
+ const extra: Record<string, unknown> = {};
4951
+ let text: string;
4952
+ if (isChartPoint(v0)) {
4953
+ const xy = pointXY(v0);
4954
+ extra.x = xy.x;
4955
+ extra.y = xy.y;
4956
+ text = drawText(node, env, pointKw != null ? 0 : 1, ["text"]);
4957
+ } else {
4958
+ putFinite(extra, "x", a0 == null ? 0 : unwrap(v0));
4959
+ putFinite(extra, "y", optionalCoord(node, env, 1, ["y"], 0));
4960
+ text = drawText(node, env, 2, ["text"]);
4961
+ }
4962
+ const tooltip = drawRaw(node, env, 12, ["tooltip"]);
4963
+ if (tooltip !== undefined) extra.tooltip = tooltip;
4964
+ const style = drawRaw(node, env, 13, ["style"]);
4965
+ if (style !== undefined) extra.style = style;
4966
+ return book.labelNew(env.barIndex, text, extra);
3493
4967
  }
3494
4968
  if (fname === "box.new") {
3495
- return env.drawings.boxNew(env.barIndex);
4969
+ const extra: Record<string, unknown> = {};
4970
+ putFinite(extra, "left", optionalCoord(node, env, 0, ["left"], 0));
4971
+ putFinite(extra, "top", optionalCoord(node, env, 1, ["top"], 0));
4972
+ putFinite(extra, "right", optionalCoord(node, env, 2, ["right"], 0));
4973
+ putFinite(extra, "bottom", optionalCoord(node, env, 3, ["bottom"], 0));
4974
+ const bgcolor = drawRaw(node, env, 6, ["bgcolor"]);
4975
+ if (bgcolor !== undefined) extra.bgcolor = bgcolor;
4976
+ const text = callArg(node.args, -1, ["text"]);
4977
+ if (text != null) extra.text = drawText(node, env, -1, ["text"]);
4978
+ return book.boxNew(env.barIndex, extra);
3496
4979
  }
3497
4980
  if (fname === "alert") {
3498
4981
  const msg = evalAlertMessage(callArg(node.args, 0, ["message"]), env);
3499
- env.drawings.alert(env.barIndex, msg);
4982
+ book.alert(env.barIndex, msg);
3500
4983
  return NA;
3501
4984
  }
3502
- if (fname === "table.new") return env.drawings.tableNew(env.barIndex);
3503
- if (fname === "polyline.new") return env.drawings.polylineNew(env.barIndex);
3504
- if (fname === "linefill.new") {
3505
- const a = unwrap(evalExpr(callArg(node.args, 0, ["id1"]), env));
3506
- const b = unwrap(evalExpr(callArg(node.args, 1, ["id2"]), env));
3507
- return env.drawings.linefillNew(
3508
- env.barIndex,
3509
- a == null ? undefined : a,
3510
- b == null ? undefined : b,
3511
- );
4985
+ if (fname === "table.new") {
4986
+ const extra: Record<string, unknown> = {};
4987
+ const position = drawRaw(node, env, 0, ["position"]);
4988
+ if (position !== undefined) extra.position = position;
4989
+ putFinite(extra, "rows", drawNum(node, env, 1, ["rows"]));
4990
+ putFinite(extra, "columns", drawNum(node, env, 2, ["columns"]));
4991
+ return book.tableNew(env.barIndex, extra);
4992
+ }
4993
+ if (fname === "polyline.new") {
4994
+ const extra: Record<string, unknown> = {};
4995
+ const ptsExpr = callArg(node.args, 0, ["points"]);
4996
+ extra.points = ptsExpr == null ? [] : pointsFromValue(evalExpr(ptsExpr, env));
4997
+ const closed = drawRaw(node, env, 1, ["closed"]);
4998
+ if (closed !== undefined) extra.closed = closed;
4999
+ const color = drawRaw(node, env, 3, ["color"]);
5000
+ if (color !== undefined) extra.color = color;
5001
+ putFinite(extra, "width", drawNum(node, env, 4, ["width"]));
5002
+ return book.polylineNew(env.barIndex, extra);
5003
+ }
5004
+ if (fname === "linefill.new" || fname === "line.fill") {
5005
+ const a = unwrap(evalExpr(callArg(node.args, 0, ["id1", "line1"]), env));
5006
+ const b = unwrap(evalExpr(callArg(node.args, 1, ["id2", "line2"]), env));
5007
+ const extra: Record<string, unknown> = {};
5008
+ if (a != null) extra.id1 = a;
5009
+ if (b != null) extra.id2 = b;
5010
+ const color = drawRaw(node, env, 2, ["color"]);
5011
+ if (color !== undefined) extra.color = color;
5012
+ return book.linefillNew(env.barIndex, extra);
5013
+ }
5014
+
5015
+ if (fname.endsWith(".all")) {
5016
+ const kind = drawingAllKind(fname.slice(0, -4));
5017
+ if (kind != null) return drawingAllArray(env, kind);
5018
+ }
5019
+
5020
+ if (fname === "chart.point.new" || fname === "chart.point.from_time") {
5021
+ const time = drawNum(node, env, 0, ["time"]);
5022
+ const priceExpr = callArg(node.args, 1, ["price"]);
5023
+ const price = priceExpr == null ? 0 : unwrap(evalExpr(priceExpr, env));
5024
+ if (price == null) return NA;
5025
+ return makeChartPoint(null, time, price);
5026
+ }
5027
+ if (fname === "chart.point.from_index") {
5028
+ const index = drawNum(node, env, 0, ["index"]);
5029
+ const priceExpr = callArg(node.args, 1, ["price"]);
5030
+ const price = priceExpr == null ? 0 : unwrap(evalExpr(priceExpr, env));
5031
+ if (price == null || index == null) return NA;
5032
+ return makeChartPoint(Math.trunc(index), null, price);
5033
+ }
5034
+ if (fname === "chart.point.now") {
5035
+ const priceExpr = callArg(node.args, 0, ["price"]);
5036
+ const price = priceExpr == null ? 0 : unwrap(evalExpr(priceExpr, env));
5037
+ if (price == null) return NA;
5038
+ const time = unwrap(env.ctx.time) ?? env.barTimes[env.barIndex] ?? null;
5039
+ return makeChartPoint(env.barIndex, time, price);
5040
+ }
5041
+ if (fname === "chart.point.copy") {
5042
+ const raw = evalExpr(callArg(node.args, 0, ["point"]), env);
5043
+ if (isChartPoint(raw)) return makeChartPoint(raw.index, raw.time, raw.price);
5044
+ return makeChartPoint(null, null, 0);
5045
+ }
5046
+
5047
+ if (fname === "line.copy" || fname === "label.copy" || fname === "box.copy" || fname === "polyline.copy") {
5048
+ const id = drawId(node, env);
5049
+ if (id == null) return NA;
5050
+ const copied = book.copy(id);
5051
+ return copied == null ? NA : copied;
5052
+ }
5053
+
5054
+ if (fname === "line.delete" || fname === "label.delete" || fname === "box.delete" || fname === "table.delete" || fname === "polyline.delete" || fname === "linefill.delete") {
5055
+ const id = drawId(node, env);
5056
+ if (id != null) {
5057
+ if (fname === "line.delete") book.lineDelete(id);
5058
+ else if (fname === "label.delete") book.labelDelete(id);
5059
+ else if (fname === "box.delete") book.boxDelete(id);
5060
+ else if (fname === "table.delete") book.tableDelete(id);
5061
+ else if (fname === "polyline.delete") book.polylineDelete(id);
5062
+ else book.linefillDelete(id);
5063
+ }
5064
+ return NA;
3512
5065
  }
5066
+
5067
+ if (fname === "line.get_x1") {
5068
+ const id = drawId(node, env);
5069
+ return id != null ? book.lineGetX1(id) : NA;
5070
+ }
5071
+ if (fname === "line.get_y1") {
5072
+ const id = drawId(node, env);
5073
+ return id != null ? book.lineGetY1(id) : NA;
5074
+ }
5075
+ if (fname === "line.get_x2") {
5076
+ const id = drawId(node, env);
5077
+ return id != null ? book.lineGetX2(id) : NA;
5078
+ }
5079
+ if (fname === "line.get_y2") {
5080
+ const id = drawId(node, env);
5081
+ return id != null ? book.lineGetY2(id) : NA;
5082
+ }
5083
+ if (fname === "line.get_price") {
5084
+ const id = drawId(node, env);
5085
+ const x = drawNum(node, env, 1, ["x"]);
5086
+ return id != null && x != null ? book.lineGetPrice(id, x) : NA;
5087
+ }
5088
+ if (fname === "label.get_x") {
5089
+ const id = drawId(node, env);
5090
+ return id == null ? 0 : (book.labelGetX(id) ?? 0);
5091
+ }
5092
+ if (fname === "label.get_y") {
5093
+ const id = drawId(node, env);
5094
+ return id == null ? 0 : (book.labelGetY(id) ?? 0);
5095
+ }
5096
+ if (fname === "label.get_text") {
5097
+ const id = drawId(node, env);
5098
+ return id == null ? "" : (book.labelGetText(id) ?? "");
5099
+ }
5100
+ if (fname === "box.get_left") {
5101
+ const id = drawId(node, env);
5102
+ return id == null ? 0 : (book.boxGetLeft(id) ?? 0);
5103
+ }
5104
+ if (fname === "box.get_right") {
5105
+ const id = drawId(node, env);
5106
+ return id == null ? 0 : (book.boxGetRight(id) ?? 0);
5107
+ }
5108
+ if (fname === "box.get_top") {
5109
+ const id = drawId(node, env);
5110
+ return id == null ? 0 : (book.boxGetTop(id) ?? 0);
5111
+ }
5112
+ if (fname === "box.get_bottom") {
5113
+ const id = drawId(node, env);
5114
+ return id == null ? 0 : (book.boxGetBottom(id) ?? 0);
5115
+ }
5116
+ if (fname === "table.cell_get_text") {
5117
+ const id = drawId(node, env);
5118
+ const row = drawNum(node, env, 1, ["row"]);
5119
+ const col = drawNum(node, env, 2, ["column"]);
5120
+ if (id == null) return "";
5121
+ return book.tableCellGetText(id, col, row) ?? "";
5122
+ }
5123
+ if (fname === "linefill.get_line1") {
5124
+ const id = drawId(node, env);
5125
+ return id != null ? book.linefillGetLine1(id) : NA;
5126
+ }
5127
+ if (fname === "linefill.get_line2") {
5128
+ const id = drawId(node, env);
5129
+ return id != null ? book.linefillGetLine2(id) : NA;
5130
+ }
5131
+ if (fname === "polyline.get_points") {
5132
+ const id = drawId(node, env);
5133
+ const arr = new PineArray();
5134
+ if (id != null) for (const p of book.polylineGetPoints(id)) arr.push(p);
5135
+ return arr;
5136
+ }
5137
+
3513
5138
  if (fname === "line.set_xy") {
3514
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
5139
+ const id = drawId(node, env);
3515
5140
  if (id != null) {
3516
- env.drawings.lineSetXy(
5141
+ book.lineSetXy(
3517
5142
  id,
3518
- unwrap(evalExpr(callArg(node.args, 1, ["x1"]), env)) ?? 0,
3519
- unwrap(evalExpr(callArg(node.args, 2, ["y1"]), env)) ?? 0,
3520
- unwrap(evalExpr(callArg(node.args, 3, ["x2"]), env)) ?? 0,
3521
- unwrap(evalExpr(callArg(node.args, 4, ["y2"]), env)) ?? 0,
5143
+ drawNum(node, env, 1, ["x1"]) ?? 0,
5144
+ drawNum(node, env, 2, ["y1"]) ?? 0,
5145
+ drawNum(node, env, 3, ["x2"]) ?? 0,
5146
+ drawNum(node, env, 4, ["y2"]) ?? 0,
3522
5147
  );
3523
5148
  }
3524
5149
  return NA;
3525
5150
  }
3526
- if (fname === "label.set_text") {
3527
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3528
- if (id != null) env.drawings.labelSetText(id, evalAsString(callArg(node.args, 1, ["text"]), env) ?? "");
5151
+ if (fname === "line.set_xy1") {
5152
+ const id = drawId(node, env);
5153
+ if (id != null) book.lineSetXy1(id, drawNum(node, env, 1, ["x1", "x"]) ?? 0, drawNum(node, env, 2, ["y1", "y"]) ?? 0);
3529
5154
  return NA;
3530
5155
  }
3531
- if (fname === "line.delete" || fname === "label.delete" || fname === "box.delete") {
3532
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3533
- if (id != null) {
3534
- if (fname === "line.delete") env.drawings.lineDelete(id);
3535
- else if (fname === "label.delete") env.drawings.labelDelete(id);
3536
- else env.drawings.boxDelete(id);
3537
- }
5156
+ if (fname === "line.set_xy2") {
5157
+ const id = drawId(node, env);
5158
+ if (id != null) book.lineSetXy2(id, drawNum(node, env, 1, ["x2", "x"]) ?? 0, drawNum(node, env, 2, ["y2", "y"]) ?? 0);
5159
+ return NA;
5160
+ }
5161
+ if (fname === "line.set_x1") {
5162
+ const id = drawId(node, env);
5163
+ if (id != null) book.lineSetX1(id, drawNum(node, env, 1, ["x1", "x"]));
5164
+ return NA;
5165
+ }
5166
+ if (fname === "line.set_y1") {
5167
+ const id = drawId(node, env);
5168
+ if (id != null) book.lineSetY1(id, drawNum(node, env, 1, ["y1", "y"]));
5169
+ return NA;
5170
+ }
5171
+ if (fname === "line.set_x2") {
5172
+ const id = drawId(node, env);
5173
+ if (id != null) book.lineSetX2(id, drawNum(node, env, 1, ["x2", "x"]));
5174
+ return NA;
5175
+ }
5176
+ if (fname === "line.set_y2") {
5177
+ const id = drawId(node, env);
5178
+ if (id != null) book.lineSetY2(id, drawNum(node, env, 1, ["y2", "y"]));
3538
5179
  return NA;
3539
5180
  }
3540
5181
  if (fname === "line.set_color") {
3541
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3542
- if (id != null) env.drawings.lineSetColor(id, evalAsString(callArg(node.args, 1, ["color"]), env) ?? "");
5182
+ const id = drawId(node, env);
5183
+ if (id != null) book.lineSetColor(id, drawRaw(node, env, 1, ["color"]));
3543
5184
  return NA;
3544
5185
  }
3545
- if (fname === "line.get_price") {
3546
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3547
- const x = unwrap(evalExpr(callArg(node.args, 1, ["x"]), env));
3548
- return id != null && x != null ? env.drawings.lineGetPrice(id, x) : NA;
5186
+ if (fname === "line.set_width") {
5187
+ const id = drawId(node, env);
5188
+ if (id != null) book.lineSetWidth(id, drawRaw(node, env, 1, ["width"]));
5189
+ return NA;
3549
5190
  }
3550
- if (fname === "line.get_x1") {
3551
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3552
- return id != null ? env.drawings.getX1(id) : NA;
5191
+ if (fname === "line.set_style") {
5192
+ const id = drawId(node, env);
5193
+ if (id != null) book.lineSetStyle(id, drawRaw(node, env, 1, ["style"]));
5194
+ return NA;
3553
5195
  }
3554
- if (fname === "line.get_y1") {
3555
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3556
- return id != null ? env.drawings.getY1(id) : NA;
5196
+ if (fname === "line.set_extend") {
5197
+ const id = drawId(node, env);
5198
+ if (id != null) book.lineSetExtend(id, drawRaw(node, env, 1, ["extend"]));
5199
+ return NA;
3557
5200
  }
3558
- if (fname === "line.get_x2") {
3559
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3560
- return id != null ? env.drawings.getX2(id) : NA;
5201
+ if (fname === "line.set_xloc") {
5202
+ const id = drawId(node, env);
5203
+ if (id != null) book.lineSetXloc(id, drawRaw(node, env, 1, ["xloc"]));
5204
+ return NA;
3561
5205
  }
3562
- if (fname === "line.get_y2") {
3563
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3564
- return id != null ? env.drawings.getY2(id) : NA;
5206
+ if (fname === "line.set_first_point") {
5207
+ const id = drawId(node, env);
5208
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5209
+ if (id != null && isChartPoint(p)) {
5210
+ const xy = pointXY(p);
5211
+ book.lineSetXy1(id, xy.x, xy.y);
5212
+ }
5213
+ return NA;
3565
5214
  }
3566
- if (fname === "label.get_text") {
3567
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3568
- return id != null ? env.drawings.labelGetText(id) : NA;
5215
+ if (fname === "line.set_second_point") {
5216
+ const id = drawId(node, env);
5217
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5218
+ if (id != null && isChartPoint(p)) {
5219
+ const xy = pointXY(p);
5220
+ book.lineSetXy2(id, xy.x, xy.y);
5221
+ }
5222
+ return NA;
5223
+ }
5224
+
5225
+ if (fname === "label.set_text") {
5226
+ const id = drawId(node, env);
5227
+ if (id != null) book.labelSetText(id, drawText(node, env, 1, ["text"]));
5228
+ return NA;
3569
5229
  }
3570
5230
  if (fname === "label.set_xy") {
3571
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
5231
+ const id = drawId(node, env);
5232
+ if (id != null) book.labelSetXy(id, drawNum(node, env, 1, ["x"]) ?? 0, drawNum(node, env, 2, ["y"]) ?? 0);
5233
+ return NA;
5234
+ }
5235
+ if (fname === "label.set_x") {
5236
+ const id = drawId(node, env);
5237
+ if (id != null) book.labelSetX(id, drawNum(node, env, 1, ["x"]));
5238
+ return NA;
5239
+ }
5240
+ if (fname === "label.set_y") {
5241
+ const id = drawId(node, env);
5242
+ if (id != null) book.labelSetY(id, drawNum(node, env, 1, ["y"]));
5243
+ return NA;
5244
+ }
5245
+ if (fname === "label.set_color") {
5246
+ const id = drawId(node, env);
5247
+ if (id != null) book.labelSetColor(id, drawRaw(node, env, 1, ["color"]));
5248
+ return NA;
5249
+ }
5250
+ if (fname === "label.set_style") {
5251
+ const id = drawId(node, env);
5252
+ if (id != null) book.labelSetStyle(id, drawRaw(node, env, 1, ["style"]));
5253
+ return NA;
5254
+ }
5255
+ if (fname === "label.set_size") {
5256
+ const id = drawId(node, env);
5257
+ if (id != null) book.labelSetSize(id, drawRaw(node, env, 1, ["size"]));
5258
+ return NA;
5259
+ }
5260
+ if (fname === "label.set_tooltip") {
5261
+ const id = drawId(node, env);
5262
+ if (id != null) book.labelSetTooltip(id, drawRaw(node, env, 1, ["tooltip"]));
5263
+ return NA;
5264
+ }
5265
+ if (fname === "label.set_textalign") {
5266
+ const id = drawId(node, env);
5267
+ if (id != null) book.labelSetTextalign(id, drawRaw(node, env, 1, ["textalign", "halign"]));
5268
+ return NA;
5269
+ }
5270
+ if (fname === "label.set_textcolor") {
5271
+ const id = drawId(node, env);
5272
+ if (id != null) book.labelSetTextcolor(id, drawRaw(node, env, 1, ["color", "textcolor"]));
5273
+ return NA;
5274
+ }
5275
+ if (fname === "label.set_text_font_family") {
5276
+ const id = drawId(node, env);
5277
+ if (id != null) book.labelSetTextFontFamily(id, drawRaw(node, env, 1, ["font_family", "text_font_family"]));
5278
+ return NA;
5279
+ }
5280
+ if (fname === "label.set_text_halign") {
5281
+ const id = drawId(node, env);
5282
+ if (id != null) book.labelSetTextHalign(id, drawRaw(node, env, 1, ["halign", "text_halign"]));
5283
+ return NA;
5284
+ }
5285
+ if (fname === "label.set_text_valign") {
5286
+ const id = drawId(node, env);
5287
+ if (id != null) book.labelSetTextValign(id, drawRaw(node, env, 1, ["valign", "text_valign"]));
5288
+ return NA;
5289
+ }
5290
+ if (fname === "label.set_text_size") {
5291
+ const id = drawId(node, env);
5292
+ if (id != null) book.labelSetTextSize(id, drawRaw(node, env, 1, ["size", "text_size"]));
5293
+ return NA;
5294
+ }
5295
+ if (fname === "label.set_text_formatting") {
5296
+ const id = drawId(node, env);
5297
+ if (id != null) book.labelSetTextFormatting(id, drawRaw(node, env, 1, ["formatting", "text_formatting"]));
5298
+ return NA;
5299
+ }
5300
+ if (fname === "label.set_border_color") {
5301
+ const id = drawId(node, env);
5302
+ if (id != null) book.labelSetBorderColor(id, drawRaw(node, env, 1, ["color"]));
5303
+ return NA;
5304
+ }
5305
+ if (fname === "label.set_border_width") {
5306
+ const id = drawId(node, env);
5307
+ if (id != null) book.labelSetBorderWidth(id, drawRaw(node, env, 1, ["width"]));
5308
+ return NA;
5309
+ }
5310
+ if (fname === "label.set_border_style") {
5311
+ const id = drawId(node, env);
5312
+ if (id != null) book.labelSetBorderStyle(id, drawRaw(node, env, 1, ["style"]));
5313
+ return NA;
5314
+ }
5315
+ if (fname === "label.set_xloc") {
5316
+ const id = drawId(node, env);
5317
+ if (id != null) book.labelSetXloc(id, drawRaw(node, env, 1, ["xloc"]));
5318
+ return NA;
5319
+ }
5320
+ if (fname === "label.set_yloc") {
5321
+ const id = drawId(node, env);
5322
+ if (id != null) book.labelSetYloc(id, drawRaw(node, env, 1, ["yloc"]));
5323
+ return NA;
5324
+ }
5325
+ if (fname === "label.set_point") {
5326
+ const id = drawId(node, env);
5327
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5328
+ if (id != null && isChartPoint(p)) {
5329
+ const xy = pointXY(p);
5330
+ book.labelSetXy(id, xy.x, xy.y);
5331
+ }
5332
+ return NA;
5333
+ }
5334
+
5335
+ if (fname === "box.set_lefttop" || fname === "box.set_corners") {
5336
+ const id = drawId(node, env);
3572
5337
  if (id != null) {
3573
- env.drawings.labelSetXy(
5338
+ book.boxSetCorners(
3574
5339
  id,
3575
- unwrap(evalExpr(callArg(node.args, 1, ["x"]), env)) ?? 0,
3576
- unwrap(evalExpr(callArg(node.args, 2, ["y"]), env)) ?? 0,
5340
+ drawNum(node, env, 1, ["left", "x1"]) ?? 0,
5341
+ drawNum(node, env, 2, ["top", "y1"]) ?? 0,
5342
+ drawNum(node, env, 3, ["right", "x2"]) ?? 0,
5343
+ drawNum(node, env, 4, ["bottom", "y2"]) ?? 0,
3577
5344
  );
3578
5345
  }
3579
5346
  return NA;
3580
5347
  }
3581
- if (fname === "box.set_lefttop" || fname === "box.set_corners") {
3582
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
5348
+ if (fname === "box.set_left") {
5349
+ const id = drawId(node, env);
5350
+ if (id != null) book.boxSetLeft(id, drawNum(node, env, 1, ["left"]));
5351
+ return NA;
5352
+ }
5353
+ if (fname === "box.set_right") {
5354
+ const id = drawId(node, env);
5355
+ if (id != null) book.boxSetRight(id, drawNum(node, env, 1, ["right"]));
5356
+ return NA;
5357
+ }
5358
+ if (fname === "box.set_top") {
5359
+ const id = drawId(node, env);
5360
+ if (id != null) book.boxSetTop(id, drawNum(node, env, 1, ["top"]));
5361
+ return NA;
5362
+ }
5363
+ if (fname === "box.set_bottom") {
5364
+ const id = drawId(node, env);
5365
+ if (id != null) book.boxSetBottom(id, drawNum(node, env, 1, ["bottom"]));
5366
+ return NA;
5367
+ }
5368
+ if (fname === "box.set_bgcolor") {
5369
+ const id = drawId(node, env);
5370
+ if (id != null) book.boxSetBgcolor(id, drawRaw(node, env, 1, ["color", "bgcolor"]));
5371
+ return NA;
5372
+ }
5373
+ if (fname === "box.set_border_color") {
5374
+ const id = drawId(node, env);
5375
+ if (id != null) book.boxSetBorderColor(id, drawRaw(node, env, 1, ["color"]));
5376
+ return NA;
5377
+ }
5378
+ if (fname === "box.set_border_width") {
5379
+ const id = drawId(node, env);
5380
+ if (id != null) book.boxSetBorderWidth(id, drawRaw(node, env, 1, ["width"]));
5381
+ return NA;
5382
+ }
5383
+ if (fname === "box.set_border_style") {
5384
+ const id = drawId(node, env);
5385
+ if (id != null) book.boxSetBorderStyle(id, drawRaw(node, env, 1, ["style"]));
5386
+ return NA;
5387
+ }
5388
+ if (fname === "box.set_extend") {
5389
+ const id = drawId(node, env);
5390
+ if (id != null) book.boxSetExtend(id, drawRaw(node, env, 1, ["extend"]));
5391
+ return NA;
5392
+ }
5393
+ if (fname === "box.set_xloc") {
5394
+ const id = drawId(node, env);
3583
5395
  if (id != null) {
3584
- env.drawings.boxSetCorners(
5396
+ book.boxSetLeft(id, drawNum(node, env, 1, ["left"]));
5397
+ book.boxSetRight(id, drawNum(node, env, 2, ["right"]));
5398
+ book.boxSetXloc(id, drawRaw(node, env, 3, ["xloc"]));
5399
+ }
5400
+ return NA;
5401
+ }
5402
+ if (fname === "box.set_closed") {
5403
+ const id = drawId(node, env);
5404
+ if (id != null) book.boxSetClosed(id, drawRaw(node, env, 1, ["closed"]));
5405
+ return NA;
5406
+ }
5407
+ if (fname === "box.set_rightbottom") {
5408
+ const id = drawId(node, env);
5409
+ if (id != null) book.boxSetRightBottom(id, drawNum(node, env, 1, ["right"]), drawNum(node, env, 2, ["bottom"]));
5410
+ return NA;
5411
+ }
5412
+ if (fname === "box.set_text") {
5413
+ const id = drawId(node, env);
5414
+ if (id != null) book.boxSetText(id, drawText(node, env, 1, ["text"]));
5415
+ return NA;
5416
+ }
5417
+ if (fname === "box.set_text_color") {
5418
+ const id = drawId(node, env);
5419
+ if (id != null) book.boxSetTextColor(id, drawRaw(node, env, 1, ["color", "text_color"]));
5420
+ return NA;
5421
+ }
5422
+ if (fname === "box.set_text_font_family") {
5423
+ const id = drawId(node, env);
5424
+ if (id != null) book.boxSetTextFontFamily(id, drawRaw(node, env, 1, ["font_family"]));
5425
+ return NA;
5426
+ }
5427
+ if (fname === "box.set_text_halign") {
5428
+ const id = drawId(node, env);
5429
+ if (id != null) book.boxSetTextHalign(id, drawRaw(node, env, 1, ["halign"]));
5430
+ return NA;
5431
+ }
5432
+ if (fname === "box.set_text_valign") {
5433
+ const id = drawId(node, env);
5434
+ if (id != null) book.boxSetTextValign(id, drawRaw(node, env, 1, ["valign"]));
5435
+ return NA;
5436
+ }
5437
+ if (fname === "box.set_text_size") {
5438
+ const id = drawId(node, env);
5439
+ if (id != null) book.boxSetTextSize(id, drawRaw(node, env, 1, ["size"]));
5440
+ return NA;
5441
+ }
5442
+ if (fname === "box.set_text_formatting") {
5443
+ const id = drawId(node, env);
5444
+ if (id != null) book.boxSetTextFormatting(id, drawRaw(node, env, 1, ["formatting"]));
5445
+ return NA;
5446
+ }
5447
+ if (fname === "box.set_text_wrap") {
5448
+ const id = drawId(node, env);
5449
+ if (id != null) book.boxSetTextWrap(id, drawRaw(node, env, 1, ["wrap"]));
5450
+ return NA;
5451
+ }
5452
+ if (fname === "box.set_top_left_point") {
5453
+ const id = drawId(node, env);
5454
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5455
+ if (id != null && isChartPoint(p)) {
5456
+ const xy = pointXY(p);
5457
+ book.boxSetLeftTop(id, xy.x, xy.y);
5458
+ }
5459
+ return NA;
5460
+ }
5461
+ if (fname === "box.set_bottom_right_point") {
5462
+ const id = drawId(node, env);
5463
+ const p = evalExpr(callArg(node.args, 1, ["point"]), env);
5464
+ if (id != null && isChartPoint(p)) {
5465
+ const xy = pointXY(p);
5466
+ book.boxSetRightBottom(id, xy.x, xy.y);
5467
+ }
5468
+ return NA;
5469
+ }
5470
+
5471
+ if (fname === "table.cell") {
5472
+ const id = drawId(node, env);
5473
+ const col = drawNum(node, env, 1, ["column"]);
5474
+ const row = drawNum(node, env, 2, ["row"]);
5475
+ const textArg = callArg(node.args, 3, ["text"]);
5476
+ const text = textArg == null ? undefined : drawText(node, env, 3, ["text"]);
5477
+ if (id != null && col != null && row != null) book.tableCell(id, col, row, text);
5478
+ return NA;
5479
+ }
5480
+ if (fname === "table.cell_set_text") {
5481
+ const id = drawId(node, env);
5482
+ const row = drawNum(node, env, 1, ["row"]);
5483
+ const col = drawNum(node, env, 2, ["column"]);
5484
+ if (id != null) book.tableCellSetText(id, col, row, drawText(node, env, 3, ["text"]));
5485
+ return NA;
5486
+ }
5487
+ if (
5488
+ fname === "table.cell_set_text_color" ||
5489
+ fname === "table.cell_set_bgcolor" ||
5490
+ fname === "table.cell_set_border_color" ||
5491
+ fname === "table.cell_set_border_width" ||
5492
+ fname === "table.cell_set_tooltip"
5493
+ ) {
5494
+ const id = drawId(node, env);
5495
+ const row = drawNum(node, env, 1, ["row"]);
5496
+ const col = drawNum(node, env, 2, ["column"]);
5497
+ const field =
5498
+ fname === "table.cell_set_text_color"
5499
+ ? "text_color"
5500
+ : fname === "table.cell_set_bgcolor"
5501
+ ? "bgcolor"
5502
+ : fname === "table.cell_set_border_color"
5503
+ ? "border_color"
5504
+ : fname === "table.cell_set_border_width"
5505
+ ? "border_width"
5506
+ : "tooltip";
5507
+ if (id != null) book.tableCellSetField(id, col, row, field, drawRaw(node, env, 3, ["color", "width", "tooltip", "text"]));
5508
+ return NA;
5509
+ }
5510
+ if (
5511
+ fname === "table.cell_set_width" ||
5512
+ fname === "table.cell_set_height" ||
5513
+ fname === "table.cell_set_text_halign" ||
5514
+ fname === "table.cell_set_text_valign" ||
5515
+ fname === "table.cell_set_text_size" ||
5516
+ fname === "table.cell_set_text_font_family" ||
5517
+ fname === "table.cell_set_text_formatting"
5518
+ ) {
5519
+ const id = drawId(node, env);
5520
+ const col = drawNum(node, env, 1, ["column", "col"]);
5521
+ const row = drawNum(node, env, 2, ["row"]);
5522
+ const field = fname.slice("table.cell_set_".length);
5523
+ if (id != null) book.tableCellSetField(id, col, row, field, drawRaw(node, env, 3, ["width", "height", "halign", "valign", "size", "font_family", "formatting"]));
5524
+ return NA;
5525
+ }
5526
+ if (fname === "table.set_position") {
5527
+ const id = drawId(node, env);
5528
+ if (id != null) book.tableSetPosition(id, drawRaw(node, env, 1, ["position"]));
5529
+ return NA;
5530
+ }
5531
+ if (fname === "table.set_bgcolor") {
5532
+ const id = drawId(node, env);
5533
+ if (id != null) book.tableSetBgcolor(id, drawRaw(node, env, 1, ["color", "bgcolor"]));
5534
+ return NA;
5535
+ }
5536
+ if (fname === "table.set_border_color") {
5537
+ const id = drawId(node, env);
5538
+ if (id != null) book.tableSetBorderColor(id, drawRaw(node, env, 1, ["color"]));
5539
+ return NA;
5540
+ }
5541
+ if (fname === "table.set_border_width") {
5542
+ const id = drawId(node, env);
5543
+ if (id != null) book.tableSetBorderWidth(id, drawRaw(node, env, 1, ["width"]));
5544
+ return NA;
5545
+ }
5546
+ if (fname === "table.set_frame_color") {
5547
+ const id = drawId(node, env);
5548
+ if (id != null) book.tableSetFrameColor(id, drawRaw(node, env, 1, ["color"]));
5549
+ return NA;
5550
+ }
5551
+ if (fname === "table.set_frame_width") {
5552
+ const id = drawId(node, env);
5553
+ if (id != null) book.tableSetFrameWidth(id, drawRaw(node, env, 1, ["width"]));
5554
+ return NA;
5555
+ }
5556
+ if (fname === "table.clear") {
5557
+ const id = drawId(node, env);
5558
+ if (id != null) {
5559
+ const a1 = callArg(node.args, 1, ["start_row"]);
5560
+ if (a1 == null) book.tableClear(id);
5561
+ else {
5562
+ book.tableClear(
5563
+ id,
5564
+ drawNum(node, env, 1, ["start_row"]),
5565
+ drawNum(node, env, 2, ["start_column", "start_col"]),
5566
+ drawNum(node, env, 3, ["end_row"]),
5567
+ drawNum(node, env, 4, ["end_column", "end_col"]),
5568
+ );
5569
+ }
5570
+ }
5571
+ return NA;
5572
+ }
5573
+ if (fname === "table.merge_cells") {
5574
+ const id = drawId(node, env);
5575
+ if (id != null) {
5576
+ book.tableMergeCells(
3585
5577
  id,
3586
- unwrap(evalExpr(callArg(node.args, 1, ["left", "x1"]), env)) ?? 0,
3587
- unwrap(evalExpr(callArg(node.args, 2, ["top", "y1"]), env)) ?? 0,
3588
- unwrap(evalExpr(callArg(node.args, 3, ["right", "x2"]), env)) ?? 0,
3589
- unwrap(evalExpr(callArg(node.args, 4, ["bottom", "y2"]), env)) ?? 0,
5578
+ drawNum(node, env, 1, ["start_row"]),
5579
+ drawNum(node, env, 2, ["start_column", "start_col"]),
5580
+ drawNum(node, env, 3, ["end_row"]),
5581
+ drawNum(node, env, 4, ["end_column", "end_col"]),
3590
5582
  );
3591
5583
  }
3592
5584
  return NA;
3593
5585
  }
3594
- if (fname === "label.set_color") {
3595
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3596
- if (id != null) env.drawings.labelSetColor(id, evalAsString(callArg(node.args, 1, ["color"]), env) ?? "");
5586
+
5587
+ if (fname === "linefill.set_color") {
5588
+ const id = drawId(node, env);
5589
+ if (id != null) book.linefillSetColor(id, drawRaw(node, env, 1, ["color"]));
3597
5590
  return NA;
3598
5591
  }
3599
- if (fname === "table.cell") {
3600
- const id = unwrap(evalExpr(callArg(node.args, 0, ["id"]), env));
3601
- const col = unwrap(evalExpr(callArg(node.args, 1, ["column"]), env));
3602
- const row = unwrap(evalExpr(callArg(node.args, 2, ["row"]), env));
3603
- const text = evalAsString(callArg(node.args, 3, ["text"]), env) ?? undefined;
3604
- if (id != null && col != null && row != null) env.drawings.tableCell(id, col, row, text);
5592
+ if (fname === "polyline.set_points") {
5593
+ const id = drawId(node, env);
5594
+ if (id != null) book.polylineSetPoints(id, pointsFromValue(evalExpr(callArg(node.args, 1, ["points"]), env)));
5595
+ return NA;
5596
+ }
5597
+ if (fname === "polyline.set_line_color") {
5598
+ const id = drawId(node, env);
5599
+ if (id != null) book.polylineSetLineColor(id, drawRaw(node, env, 1, ["color"]));
5600
+ return NA;
5601
+ }
5602
+ if (fname === "polyline.set_line_width") {
5603
+ const id = drawId(node, env);
5604
+ if (id != null) book.polylineSetLineWidth(id, drawRaw(node, env, 1, ["width"]));
5605
+ return NA;
5606
+ }
5607
+ if (fname === "polyline.set_line_style") {
5608
+ const id = drawId(node, env);
5609
+ if (id != null) book.polylineSetLineStyle(id, drawRaw(node, env, 1, ["style"]));
5610
+ return NA;
5611
+ }
5612
+ if (fname === "polyline.set_fill_color") {
5613
+ const id = drawId(node, env);
5614
+ if (id != null) book.polylineSetFillColor(id, drawRaw(node, env, 1, ["fill_color", "color"]));
5615
+ return NA;
5616
+ }
5617
+ if (fname === "polyline.set_curved") {
5618
+ const id = drawId(node, env);
5619
+ if (id != null) book.polylineSetCurved(id, drawRaw(node, env, 1, ["curved"]));
5620
+ return NA;
5621
+ }
5622
+ if (fname === "polyline.set_force_overlay") {
5623
+ const id = drawId(node, env);
5624
+ if (id != null) book.polylineSetForceOverlay(id, drawRaw(node, env, 1, ["force_overlay"]));
5625
+ return NA;
5626
+ }
5627
+ if (fname === "polyline.set_closed") {
5628
+ const id = drawId(node, env);
5629
+ if (id != null) book.polylineSetClosed(id, drawRaw(node, env, 1, ["closed"]));
5630
+ return NA;
5631
+ }
5632
+ if (fname === "polyline.set_xloc") {
5633
+ const id = drawId(node, env);
5634
+ if (id != null) book.polylineSetXloc(id, drawRaw(node, env, 1, ["xloc"]));
3605
5635
  return NA;
3606
5636
  }
3607
5637
  return undefined;
@@ -3949,9 +5979,54 @@ function evalTimeCall(fname: string | null, node: Call, env: Env): Value | undef
3949
5979
  const ms = calendarMs(node, env);
3950
5980
  return ms == null ? NA : timeTradingDay(ms);
3951
5981
  }
5982
+ if (fname === "time_close") return barTimeClose(env);
5983
+ if (fname === "timenow") {
5984
+ const seeded = env.ctx.timenow;
5985
+ if (typeof seeded === "number" && Number.isFinite(seeded)) return seeded;
5986
+ if (typeof env.lastBarTime === "number" && Number.isFinite(env.lastBarTime)) return env.lastBarTime;
5987
+ const t = env.ctx.time;
5988
+ return typeof t === "number" && Number.isFinite(t) ? t : NA;
5989
+ }
3952
5990
  return undefined;
3953
5991
  }
3954
5992
 
5993
+ function barTimeClose(env: Env): number {
5994
+ const close = env.ctx.time_close;
5995
+ if (typeof close === "number" && Number.isFinite(close)) return close;
5996
+ const open = env.ctx.time;
5997
+ return typeof open === "number" && Number.isFinite(open) ? open : 0;
5998
+ }
5999
+
6000
+ function timeframeChange(node: Call, env: Env): Cell {
6001
+ const arg = callArg(node.args, 0, ["timeframe", "tf"]);
6002
+ if (arg == null) return 0;
6003
+ const raw = evalExpr(arg, env);
6004
+ const tf = typeof raw === "string" ? raw : typeof raw === "number" && Number.isFinite(raw) ? String(raw) : null;
6005
+ if (tf == null || tf.trim() === "") return 0;
6006
+ const curr = env.ctx.time;
6007
+ if (typeof curr !== "number" || !Number.isFinite(curr)) return 0;
6008
+ const hist = env.series.get("time");
6009
+ const prev = hist != null && hist.length >= 2 ? hist.get(1) : null;
6010
+ return timeframePeriodChanged(curr, prev, tf, env.barIndex) ? 1 : 0;
6011
+ }
6012
+
6013
+ /** Python `split_symbol` / `extract_prefix`: text before the first `:`, else `""`. */
6014
+ function extractPrefix(symbol: string): string {
6015
+ const s = symbol.trim();
6016
+ const i = s.indexOf(":");
6017
+ return i < 0 ? "" : s.slice(0, i);
6018
+ }
6019
+
6020
+ function syminfoPrefix(node: Call, env: Env): string {
6021
+ const arg = callArg(node.args, 0, ["tickerid", "symbol"]);
6022
+ if (arg == null) return extractPrefix(env.symbol);
6023
+ const raw = evalExpr(arg, env);
6024
+ if (raw instanceof TickerId) return extractPrefix(raw.symbol);
6025
+ if (typeof raw === "string") return extractPrefix(raw);
6026
+ if (typeof raw === "number" && Number.isFinite(raw)) return extractPrefix(String(raw));
6027
+ return "";
6028
+ }
6029
+
3955
6030
  function calendarMs(node: Call, env: Env): number | null {
3956
6031
  const arg = callArg(node.args, 0, ["time", "timestamp"]);
3957
6032
  if (arg == null) return num(env.ctx.time);