@pineforge/codegen-pyodide 0.8.1 → 0.9.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.
@@ -59,16 +59,21 @@ tables and types come from ``codegen/tables.py``, ``..ast_nodes``,
59
59
  from __future__ import annotations
60
60
 
61
61
  from ..ast_nodes import (
62
- ASTNode, Assignment, BinOp, BreakStmt, ContinueStmt, ExprStmt, ForStmt,
63
- ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, NumberLiteral, StringLiteral,
64
- Subscript, SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
65
- WhileStmt,
62
+ ASTNode, Assignment, BinOp, BoolLiteral, BreakStmt, ContinueStmt, ExprStmt,
63
+ ForStmt, ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess,
64
+ NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary, TupleAssign,
65
+ TupleLiteral, UnaryOp, VarDecl, WhileStmt,
66
66
  )
67
67
  from ..analyzer import (
68
68
  FuncInfo, TACallSite, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
69
69
  )
70
+ from .. import signatures as sigs
70
71
  from ..symbols import PineType
71
- from .tables import PINE_TYPE_TO_CPP, SECURITY_OHLC_BAR_FIELDS
72
+ from .tables import (
73
+ MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
74
+ SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, _math_minmax_na_expr,
75
+ _merge_kwargs,
76
+ )
72
77
 
73
78
 
74
79
  class SecurityEmitter:
@@ -100,6 +105,13 @@ class SecurityEmitter:
100
105
  return self._known_vars[name], None
101
106
  if name in self._input_backed_vars and name in self._input_var_to_call:
102
107
  return None, self._visit_expr(self._input_var_to_call[name])
108
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
109
+ if name in global_expr_map:
110
+ expanded = self._security_tf_runtime_expr(
111
+ global_expr_map[name], resolving={name}
112
+ )
113
+ if expanded is not None:
114
+ return None, expanded
103
115
  # class-scope resolvable (global / input member)?
104
116
  if self._ident_is_resolvable(name):
105
117
  try:
@@ -115,10 +127,56 @@ class SecurityEmitter:
115
127
  return None, "input_tf_"
116
128
  # any other expression — visit if it resolves at class scope
117
129
  try:
118
- return None, self._visit_expr(tf_node)
130
+ expanded = self._security_tf_runtime_expr(tf_node)
131
+ return None, expanded if expanded is not None else self._visit_expr(tf_node)
119
132
  except Exception:
120
133
  return None, "input_tf_"
121
134
 
135
+ def _security_tf_runtime_expr(self, node, resolving: set[str] | None = None) -> str | None:
136
+ """Render a request.security timeframe expression for registration time.
137
+
138
+ Security evaluators are registered before ``on_bar()`` initializes global
139
+ variables, so input-backed aliases such as ``tf = useChart ? timeframe.period
140
+ : inputTf`` must be expanded to their source expression with direct
141
+ ``get_input_*`` reads. Emitting the member name would register with its
142
+ default-constructed value (usually an empty string).
143
+ """
144
+ if node is None:
145
+ return None
146
+ resolving = resolving or set()
147
+ if isinstance(node, StringLiteral):
148
+ return self._visit_expr(node)
149
+ if isinstance(node, Identifier):
150
+ name = node.name
151
+ if name in self._timeframe_period_vars:
152
+ return "script_tf_"
153
+ if name in self._input_backed_vars and name in self._input_var_to_call:
154
+ return self._visit_expr(self._input_var_to_call[name])
155
+ if (name in self._known_vars and name not in self._input_backed_vars
156
+ and isinstance(self._known_vars[name], str)):
157
+ return self._visit_expr(StringLiteral(value=self._known_vars[name]))
158
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
159
+ if name in global_expr_map and name not in resolving:
160
+ resolving.add(name)
161
+ out = self._security_tf_runtime_expr(global_expr_map[name], resolving)
162
+ resolving.remove(name)
163
+ return out
164
+ return self._visit_expr(node)
165
+ if (
166
+ isinstance(node, MemberAccess)
167
+ and isinstance(node.object, Identifier)
168
+ and node.object.name == "timeframe"
169
+ and node.member == "period"
170
+ ):
171
+ return "script_tf_"
172
+ if isinstance(node, Ternary):
173
+ cond = self._security_tf_runtime_expr(node.condition, resolving)
174
+ tv = self._security_tf_runtime_expr(node.true_val, resolving)
175
+ fv = self._security_tf_runtime_expr(node.false_val, resolving)
176
+ if cond is not None and tv is not None and fv is not None:
177
+ return f"(({cond}) ? ({tv}) : ({fv}))"
178
+ return self._visit_expr(node)
179
+
122
180
  def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
123
181
  """For a ``request.security`` whose tf is function parameter ``param_name``
124
182
  of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
@@ -276,7 +334,7 @@ class SecurityEmitter:
276
334
  return None
277
335
 
278
336
  def _literal_int_for_security_index(self, node) -> int | None:
279
- """Integer index for OHLC[ n ] inside request.security (must be literal)."""
337
+ """Integer index for bar-field[n] inside request.security (must be literal)."""
280
338
  if isinstance(node, NumberLiteral):
281
339
  v = node.value
282
340
  if isinstance(v, bool):
@@ -295,19 +353,131 @@ class SecurityEmitter:
295
353
  return None
296
354
  return None
297
355
 
356
+ # In PineForge batch backtests these barstate flags are compile-time
357
+ # constants (see support_checker / codegen.visit_expr barstate emission):
358
+ # every bar is historical, none realtime/last. The other flags
359
+ # (isfirst/isnew/isconfirmed) depend on runtime tick state, so they are
360
+ # deliberately absent and leave a fold "unknown".
361
+ _SECURITY_CONST_BARSTATE = {
362
+ "isrealtime": False,
363
+ "islast": False,
364
+ "ishistory": True,
365
+ "islastconfirmedhistory": False,
366
+ }
367
+
368
+ def _fold_security_const_bool(
369
+ self,
370
+ node,
371
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
372
+ resolving: set[str] | None = None,
373
+ ) -> bool | None:
374
+ """Fold a boolean expression to a compile-time constant, or None.
375
+
376
+ Only reduces expressions whose non-literal leaves are the batch-constant
377
+ barstate flags above; anything runtime-dependent returns None. Used to
378
+ resolve a request.security history index expressed as
379
+ ``barstate.isrealtime ? 1 : 0`` and friends."""
380
+ if resolving is None:
381
+ resolving = set()
382
+ if isinstance(node, BoolLiteral):
383
+ return bool(node.value)
384
+ if (
385
+ isinstance(node, MemberAccess)
386
+ and isinstance(node.object, Identifier)
387
+ and node.object.name == "barstate"
388
+ ):
389
+ return self._SECURITY_CONST_BARSTATE.get(node.member)
390
+ if isinstance(node, UnaryOp) and node.op == "not":
391
+ inner = self._fold_security_const_bool(
392
+ node.operand, helper_binding_stack, resolving
393
+ )
394
+ return None if inner is None else (not inner)
395
+ if isinstance(node, BinOp) and node.op in ("and", "or"):
396
+ left = self._fold_security_const_bool(
397
+ node.left, helper_binding_stack, resolving
398
+ )
399
+ right = self._fold_security_const_bool(
400
+ node.right, helper_binding_stack, resolving
401
+ )
402
+ if left is None or right is None:
403
+ return None
404
+ return (left and right) if node.op == "and" else (left or right)
405
+ if isinstance(node, Identifier):
406
+ bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
407
+ if bound is not None:
408
+ return self._fold_security_const_bool(
409
+ bound, helper_binding_stack, resolving
410
+ )
411
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
412
+ if node.name in global_expr_map and node.name not in resolving:
413
+ resolving.add(node.name)
414
+ out = self._fold_security_const_bool(
415
+ global_expr_map[node.name], helper_binding_stack, resolving
416
+ )
417
+ resolving.remove(node.name)
418
+ return out
419
+ return None
420
+
421
+ def _resolve_security_index_literal(
422
+ self,
423
+ node,
424
+ helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
425
+ resolving: set[str] | None = None,
426
+ ) -> int | None:
427
+ """Resolve a request.security TA/bar-field history index to a literal int.
428
+
429
+ Extends ``_literal_int_for_security_index`` by also resolving the index
430
+ through helper-parameter bindings and global aliases and constant-folding
431
+ a ternary whose condition is a batch-constant barstate flag (e.g.
432
+ ``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index short-
433
+ circuits on the first line, so behaviour is unchanged for every already-
434
+ literal index. Returns None when the index cannot be reduced to a
435
+ compile-time literal, so the caller keeps its existing rejection."""
436
+ direct = self._literal_int_for_security_index(node)
437
+ if direct is not None:
438
+ return direct
439
+ if resolving is None:
440
+ resolving = set()
441
+ if isinstance(node, Identifier):
442
+ bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
443
+ if bound is not None:
444
+ return self._resolve_security_index_literal(
445
+ bound, helper_binding_stack, resolving
446
+ )
447
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
448
+ if node.name in global_expr_map and node.name not in resolving:
449
+ resolving.add(node.name)
450
+ out = self._resolve_security_index_literal(
451
+ global_expr_map[node.name], helper_binding_stack, resolving
452
+ )
453
+ resolving.remove(node.name)
454
+ return out
455
+ return None
456
+ if isinstance(node, Ternary):
457
+ cond = self._fold_security_const_bool(node.condition, helper_binding_stack)
458
+ if cond is None:
459
+ return None
460
+ chosen = node.true_val if cond else node.false_val
461
+ return self._resolve_security_index_literal(
462
+ chosen, helper_binding_stack, resolving
463
+ )
464
+ return None
465
+
298
466
  def _collect_security_ohlc_hist_fields(self, node) -> set[str]:
299
- """Which OHLC fields need HTF history (subscript index >= 1) for this expression."""
467
+ """Which security bar fields need HTF history (subscript index >= 1)."""
300
468
  out: set[str] = set()
301
469
 
302
470
  def walk(n):
303
471
  if n is None:
304
472
  return
305
473
  if isinstance(n, Subscript) and isinstance(n.object, Identifier):
306
- if n.object.name in SECURITY_OHLC_BAR_FIELDS:
474
+ if n.object.name in SECURITY_BAR_FIELDS:
307
475
  idx = self._literal_int_for_security_index(n.index)
308
- # high[0] uses current HTF `bar`; high[k>=1] reads prior completed HTF
309
- # bars from Series history (filled before push in _eval_security_*).
310
- if idx is not None and idx >= 1:
476
+ # high[0]/time[0] uses current HTF `bar`; k>=1 reads prior
477
+ # completed HTF bars from Series history (filled before push
478
+ # in _eval_security_*). Dynamic indices may be >=1 at
479
+ # runtime, so they need the same backing Series.
480
+ if idx is None or idx >= 1:
311
481
  out.add(n.object.name)
312
482
  if isinstance(n, (list, tuple)):
313
483
  for x in n:
@@ -324,9 +494,32 @@ class SecurityEmitter:
324
494
  walk(node)
325
495
  return out
326
496
 
497
+ def _collect_security_ohlc_hist_fields_for_call(self, item: dict) -> set[str]:
498
+ """Collect HTF OHLC history needed by a security expression and any
499
+ mutable-global rebinds replayed inside that security evaluator."""
500
+ fields = self._collect_security_ohlc_hist_fields(item.get("expr_node"))
501
+ for name in item.get("mutable_globals", []) or []:
502
+ info = self._global_mutable_infos.get(name)
503
+ if info is None:
504
+ continue
505
+ for stmt in getattr(info, "source_stmts", []) or []:
506
+ fields |= self._collect_security_ohlc_hist_fields(stmt)
507
+ return fields
508
+
327
509
  def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
328
510
  return f"_sec{sec_id}_hist_{field}"
329
511
 
512
+ def _security_bar_hist_type(self, field: str) -> str:
513
+ return SECURITY_BAR_FIELD_TYPES.get(field, "double")
514
+
515
+ def _security_bar_field_expr(self, field: str) -> str:
516
+ return SECURITY_BAR_FIELD_EXPRS.get(field, f"bar.{field}")
517
+
518
+ @staticmethod
519
+ def _security_tuple_result_default(cpp_type: str, tuple_size: int) -> str:
520
+ vals = ", ".join("na<double>()" for _ in range(max(0, tuple_size)))
521
+ return f"{cpp_type}{{{vals}}}"
522
+
330
523
  def _collect_security_ta_hist_indices(self, node) -> set[int]:
331
524
  """Which security TA call-site indices need HTF history (subscript index >= 1).
332
525
 
@@ -362,7 +555,7 @@ class SecurityEmitter:
362
555
  if isinstance(n, Subscript):
363
556
  site = resolve_ta_site(n.object)
364
557
  if site is not None:
365
- idx_lit = self._literal_int_for_security_index(n.index)
558
+ idx_lit = self._resolve_security_index_literal(n.index)
366
559
  if idx_lit is not None and idx_lit >= 1:
367
560
  site_idx = self._ta_index_by_site_id.get(id(site))
368
561
  if site_idx is not None:
@@ -396,6 +589,172 @@ class SecurityEmitter:
396
589
  names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
397
590
  return names
398
591
 
592
+ def _collect_security_expr_hist_subscripts(
593
+ self, node, resolving: set[str] | None = None
594
+ ) -> list[Subscript]:
595
+ """Subscripted helper-call results needing security-context history."""
596
+ if node is None:
597
+ return []
598
+ if resolving is None:
599
+ resolving = set()
600
+
601
+ out: list[Subscript] = []
602
+ seen: set[int] = set()
603
+
604
+ def add(n: Subscript) -> None:
605
+ key = id(n)
606
+ if key not in seen:
607
+ seen.add(key)
608
+ out.append(n)
609
+
610
+ def walk(n) -> None:
611
+ if n is None:
612
+ return
613
+ if isinstance(n, Identifier):
614
+ global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
615
+ if n.name in global_expr_map and n.name not in resolving:
616
+ resolving.add(n.name)
617
+ walk(global_expr_map[n.name])
618
+ resolving.remove(n.name)
619
+ return
620
+ if (
621
+ isinstance(n, Subscript)
622
+ and isinstance(n.object, FuncCall)
623
+ and self._get_ta_site(n.object) is None
624
+ ):
625
+ add(n)
626
+ if isinstance(n, (list, tuple)):
627
+ for x in n:
628
+ walk(x)
629
+ return
630
+ for _k, v in getattr(n, "__dict__", {}).items():
631
+ if isinstance(v, ASTNode):
632
+ walk(v)
633
+ elif isinstance(v, (list, tuple)):
634
+ for x in v:
635
+ if isinstance(x, ASTNode):
636
+ walk(x)
637
+
638
+ walk(node)
639
+ return out
640
+
641
+ def _security_expr_hist_series_names(self, sec_id: int) -> list[str]:
642
+ names = []
643
+ for (sid, _node_id), meta in sorted(self._security_expr_hist_by_node.items()):
644
+ if sid == sec_id:
645
+ names.append(meta["name"])
646
+ return names
647
+
648
+ def _emit_security_expr_hist_members(
649
+ self, sec_id: int, expr_node, lines: list[str], mbb_suffix: str
650
+ ) -> None:
651
+ for idx, node in enumerate(self._collect_security_expr_hist_subscripts(expr_node)):
652
+ cpp_t = self._infer_type(node.object)
653
+ if cpp_t not in ("double", "int", "bool"):
654
+ cpp_t = "double"
655
+ name = f"_sec{sec_id}_expr_hist_{idx}"
656
+ self._security_expr_hist_by_node[(sec_id, id(node))] = {
657
+ "name": name,
658
+ "type": cpp_t,
659
+ }
660
+ lines.append(f" Series<{cpp_t}> {name}{mbb_suffix};")
661
+
662
+ def _build_security_math_call(
663
+ self,
664
+ sec_id: int,
665
+ func_name: str,
666
+ node: FuncCall,
667
+ ta_range,
668
+ ta_results: dict,
669
+ resolving: set[str],
670
+ security_mutable_names: set[str],
671
+ helper_binding_stack: tuple[dict[str, ASTNode], ...],
672
+ emitted_lines: list[str] | None,
673
+ ) -> str:
674
+ visit = lambda arg: self._build_security_expr(
675
+ sec_id,
676
+ arg,
677
+ ta_range,
678
+ ta_results,
679
+ resolving,
680
+ security_mutable_names,
681
+ helper_binding_stack,
682
+ emitted_lines,
683
+ )
684
+ args = _merge_kwargs(
685
+ node.args,
686
+ node.kwargs,
687
+ sigs.get_param_names("math", func_name),
688
+ visit,
689
+ )
690
+ if func_name == "round" and len(args) == 2:
691
+ return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
692
+ if func_name == "round_to_mintick":
693
+ x = args[0] if args else "0.0"
694
+ return f"round_to_mintick({x})"
695
+ if func_name == "todegrees":
696
+ x = args[0] if args else "0.0"
697
+ return f"({x} * 180.0 / M_PI)"
698
+ if func_name == "toradians":
699
+ x = args[0] if args else "0.0"
700
+ return f"({x} * M_PI / 180.0)"
701
+ if func_name == "random":
702
+ lo = args[0] if len(args) > 0 else "0.0"
703
+ hi = args[1] if len(args) > 1 else "1.0"
704
+ seed = args[2] if len(args) > 2 else "0"
705
+ call_site = self._random_call_counter
706
+ self._random_call_counter += 1
707
+ return f"pine_random({lo}, {call_site}u, {hi}, (uint32_t)({seed}), bar_index_)"
708
+ if func_name == "avg" and len(args) > 2:
709
+ sum_expr = " + ".join(f"(double)({a})" for a in args)
710
+ return f"(({sum_expr}) / {len(args)}.0)"
711
+ if func_name in ("min", "max"):
712
+ return _math_minmax_na_expr(func_name, args)
713
+ if func_name in MATH_FUNC_MAP:
714
+ mapped = MATH_FUNC_MAP[func_name]
715
+ if "{0}" in mapped:
716
+ return mapped.format(*args)
717
+ return f"{mapped}({', '.join(args)})"
718
+ return f"0.0 /* unsupported: math.{func_name} */"
719
+
720
+ def _security_timeframe_expr(self, sec_id: int) -> str:
721
+ """C++ expression for the timeframe of a request.security evaluator."""
722
+ info = self._security_eval_info[sec_id]
723
+ if info.get("tf"):
724
+ return f'"{info["tf"]}"'
725
+ if info.get("tf_expr"):
726
+ return info["tf_expr"]
727
+ return "input_tf_"
728
+
729
+ def _build_security_timeframe_member(self, sec_id: int, member: str) -> str | None:
730
+ """Lower timeframe.* reads inside request.security to the requested TF."""
731
+ tf = self._security_timeframe_expr(sec_id)
732
+ if member == "period":
733
+ return tf
734
+ if member == "main_period":
735
+ return "main_period()"
736
+ if member == "multiplier":
737
+ return f"tf_multiplier({tf})"
738
+ if member == "isintraday":
739
+ return f"tf_is_intraday({tf})"
740
+ if member == "isminutes":
741
+ return f"(tf_is_intraday({tf}) && !tf_is_seconds({tf}))"
742
+ if member == "isdaily":
743
+ return f"tf_is_daily({tf})"
744
+ if member == "isweekly":
745
+ return f"tf_is_weekly({tf})"
746
+ if member == "ismonthly":
747
+ return f"tf_is_monthly({tf})"
748
+ if member == "isdwm":
749
+ return f"(tf_is_daily({tf}) || tf_is_weekly({tf}) || tf_is_monthly({tf}))"
750
+ if member == "isseconds":
751
+ return f"tf_is_seconds({tf})"
752
+ if member == "in_seconds":
753
+ return f"tf_to_seconds({tf})"
754
+ if member == "isticks":
755
+ return "false"
756
+ return None
757
+
399
758
  @staticmethod
400
759
  def _security_series_binding(series_name: str) -> str:
401
760
  return f"@series:{series_name}"
@@ -924,6 +1283,16 @@ class SecurityEmitter:
924
1283
  if bound is not None:
925
1284
  if isinstance(bound, str):
926
1285
  return collected
1286
+ # Guard against a cyclic helper binding: an identifier bound to
1287
+ # an expression that transitively references the same binding
1288
+ # (e.g. mutually-referential helper params) would recurse here
1289
+ # forever — the mutable/global/func paths below already carry a
1290
+ # `resolving` guard, this path did not. Keyed by the bound node
1291
+ # so acyclic bindings are unaffected (byte-identical emission).
1292
+ bind_key = f"bind:{id(bound)}"
1293
+ if bind_key in resolving:
1294
+ return collected
1295
+ resolving.add(bind_key)
927
1296
  self._collect_security_ta_binding_stacks(
928
1297
  bound,
929
1298
  resolving,
@@ -932,6 +1301,7 @@ class SecurityEmitter:
932
1301
  inline_ta_indices,
933
1302
  inline_helper,
934
1303
  )
1304
+ resolving.discard(bind_key)
935
1305
  return collected
936
1306
 
937
1307
  mutable_info = self._global_mutable_infos.get(expr_node.name)
@@ -1335,7 +1705,7 @@ class SecurityEmitter:
1335
1705
  lines.append(" if (is_complete) {")
1336
1706
  for field in fields:
1337
1707
  lines.append(
1338
- f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
1708
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push({self._security_bar_field_expr(field)});"
1339
1709
  )
1340
1710
  lines.append(" }")
1341
1711
 
@@ -1535,6 +1905,8 @@ class SecurityEmitter:
1535
1905
  )
1536
1906
  for name in self._security_ta_hist_series_names(sec_id):
1537
1907
  lines.append(f" {name}.clear();")
1908
+ for name in self._security_expr_hist_series_names(sec_id):
1909
+ lines.append(f" {name}.clear();")
1538
1910
  lines.append(" break;")
1539
1911
  continue
1540
1912
  if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
@@ -1559,11 +1931,39 @@ class SecurityEmitter:
1559
1931
  )
1560
1932
  for name in self._security_ta_hist_series_names(sec_id):
1561
1933
  lines.append(f" {name}.clear();")
1934
+ for name in self._security_expr_hist_series_names(sec_id):
1935
+ lines.append(f" {name}.clear();")
1936
+ lines.append(" break;")
1937
+ elif returns_tuple and tuple_size and tuple_size > 0:
1938
+ site = self._get_ta_site(expr_node)
1939
+ ta_name = self._ta_name_from_site(site) if site is not None else ""
1940
+ ctype = {
1941
+ "macd": "ta::MACDResult",
1942
+ "supertrend": "ta::SupertrendResult",
1943
+ "dmi": "ta::DMIResult",
1944
+ "bb": "ta::BBResult",
1945
+ "kc": "ta::KCResult",
1946
+ "vwap_bands": "ta::VWAPBandsResult",
1947
+ }.get(ta_name, "std::tuple<double, double>")
1948
+ lines.append(f" case {sec_id}:")
1949
+ lines.append(
1950
+ f" _req_sec_{sec_id} = "
1951
+ f"{self._security_tuple_result_default(ctype, tuple_size)};"
1952
+ )
1953
+ for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
1954
+ lines.append(
1955
+ f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
1956
+ )
1957
+ for name in self._security_ta_hist_series_names(sec_id):
1958
+ lines.append(f" {name}.clear();")
1959
+ for name in self._security_expr_hist_series_names(sec_id):
1960
+ lines.append(f" {name}.clear();")
1562
1961
  lines.append(" break;")
1563
1962
  else:
1564
1963
  hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
1565
1964
  ta_hist_names = self._security_ta_hist_series_names(sec_id)
1566
- if hist or ta_hist_names:
1965
+ expr_hist_names = self._security_expr_hist_series_names(sec_id)
1966
+ if hist or ta_hist_names or expr_hist_names:
1567
1967
  lines.append(f" case {sec_id}:")
1568
1968
  lines.append(f" _req_sec_{sec_id} = na<double>();")
1569
1969
  for field in sorted(hist):
@@ -1572,6 +1972,8 @@ class SecurityEmitter:
1572
1972
  )
1573
1973
  for name in ta_hist_names:
1574
1974
  lines.append(f" {name}.clear();")
1975
+ for name in expr_hist_names:
1976
+ lines.append(f" {name}.clear();")
1575
1977
  lines.append(" break;")
1576
1978
  else:
1577
1979
  lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
@@ -1619,9 +2021,7 @@ class SecurityEmitter:
1619
2021
  emitted_lines,
1620
2022
  )
1621
2023
  bar_fields = {
1622
- "close": "bar.close", "high": "bar.high",
1623
- "low": "bar.low", "open": "bar.open",
1624
- "volume": "bar.volume",
2024
+ **SECURITY_BAR_FIELD_EXPRS,
1625
2025
  "hl2": "((bar.high + bar.low) / 2.0)",
1626
2026
  "hlc3": "((bar.high + bar.low + bar.close) / 3.0)",
1627
2027
  "ohlc4": "((bar.open + bar.high + bar.low + bar.close) / 4.0)",
@@ -1652,6 +2052,15 @@ class SecurityEmitter:
1652
2052
  resolving.remove(expr_node.name)
1653
2053
  return resolved
1654
2054
 
2055
+ if (
2056
+ isinstance(expr_node, MemberAccess)
2057
+ and isinstance(expr_node.object, Identifier)
2058
+ and expr_node.object.name == "timeframe"
2059
+ ):
2060
+ resolved = self._build_security_timeframe_member(sec_id, expr_node.member)
2061
+ if resolved is not None:
2062
+ return resolved
2063
+
1655
2064
  if isinstance(expr_node, Subscript):
1656
2065
  index_cpp = self._build_security_expr(
1657
2066
  sec_id,
@@ -1682,29 +2091,33 @@ class SecurityEmitter:
1682
2091
  emitted_lines,
1683
2092
  )
1684
2093
  return f"{obj_cpp}[{index_cpp}]"
1685
- if expr_node.object.name in SECURITY_OHLC_BAR_FIELDS:
2094
+ if expr_node.object.name in SECURITY_BAR_FIELDS:
2095
+ field = expr_node.object.name
1686
2096
  idx_lit = self._literal_int_for_security_index(expr_node.index)
1687
2097
  if idx_lit is not None:
1688
- bar_map = {
1689
- "open": "bar.open",
1690
- "high": "bar.high",
1691
- "low": "bar.low",
1692
- "close": "bar.close",
1693
- "volume": "bar.volume",
1694
- }
1695
2098
  if idx_lit == 0:
1696
- return bar_map[expr_node.object.name]
2099
+ return self._security_bar_field_expr(field)
1697
2100
  if idx_lit >= 1:
1698
2101
  # lookahead_off: we evaluate when an HTF bar completes; `bar` is that
1699
- # bar. On the HTF series, high[0]/close is the current (just-finished)
1700
- # bar; high[1] is one HTF bar back = hist[field][0] *before* we push
1701
- # `bar` (Series [0] = most recent prior push). high[k] -> hist[k-1].
1702
- field = expr_node.object.name
2102
+ # bar. On the HTF series, high[0]/time[0] is the current
2103
+ # (just-finished) bar; high[1]/time[1] is one HTF bar back
2104
+ # = hist[field][0] *before* we push `bar` (Series [0] =
2105
+ # most recent prior push). field[k] -> hist[k-1].
1703
2106
  hist = self._security_ohlc_hist_series_cpp(sec_id, field)
1704
2107
  return f"{hist}[{idx_lit - 1}]"
1705
- self._codegen_error(
1706
- expr_node,
1707
- "request.security() OHLC history index must be a literal integer (e.g. high[1])",
2108
+ if idx_lit is not None:
2109
+ self._codegen_error(
2110
+ expr_node,
2111
+ "request.security() bar-field history index must be non-negative",
2112
+ )
2113
+ hist = self._security_ohlc_hist_series_cpp(sec_id, field)
2114
+ cpp_t = self._security_bar_hist_type(field)
2115
+ current = self._security_bar_field_expr(field)
2116
+ return (
2117
+ f"([&]() -> {cpp_t} {{ "
2118
+ f"int _hidx = (int)({index_cpp}); "
2119
+ f"return (_hidx <= 0) ? {current} : {hist}[_hidx - 1]; "
2120
+ f"}}())"
1708
2121
  )
1709
2122
 
1710
2123
  # Indirect TA binding: ``v = ta.ema(close, 55)`` then
@@ -1734,6 +2147,31 @@ class SecurityEmitter:
1734
2147
  )
1735
2148
  resolving.remove(expr_node.object.name)
1736
2149
  return resolved
2150
+ if (
2151
+ isinstance(expr_node.object, FuncCall)
2152
+ and self._get_ta_site(expr_node.object) is None
2153
+ ):
2154
+ meta = self._security_expr_hist_by_node.get((sec_id, id(expr_node)))
2155
+ hist = meta["name"] if meta else f"_sec{sec_id}_expr_hist_missing"
2156
+ cpp_t = meta["type"] if meta else "double"
2157
+ inner = self._build_security_expr(
2158
+ sec_id,
2159
+ expr_node.object,
2160
+ ta_range,
2161
+ ta_results,
2162
+ resolving,
2163
+ security_mutable_names,
2164
+ helper_binding_stack,
2165
+ emitted_lines,
2166
+ )
2167
+ return (
2168
+ f"([&]() -> {cpp_t} {{ "
2169
+ f"{cpp_t} _hv = ({inner}); "
2170
+ f"int _hidx = (int)({index_cpp}); "
2171
+ f"{cpp_t} _out = (_hidx <= 0) ? _hv : {hist}[_hidx - 1]; "
2172
+ f"if (is_complete) {hist}.push(_hv); "
2173
+ f"return _out; }}())"
2174
+ )
1737
2175
  ta_site = self._get_ta_site(expr_node.object)
1738
2176
  if ta_site is not None:
1739
2177
  # ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
@@ -1748,7 +2186,9 @@ class SecurityEmitter:
1748
2186
  # produced the chart-tf TA instead of the confirmed HTF value.
1749
2187
  idx = self._ta_index_by_site_id.get(id(ta_site))
1750
2188
  sig = self._security_binding_stack_signature(helper_binding_stack)
1751
- idx_lit = self._literal_int_for_security_index(expr_node.index)
2189
+ idx_lit = self._resolve_security_index_literal(
2190
+ expr_node.index, helper_binding_stack
2191
+ )
1752
2192
  if idx_lit is None:
1753
2193
  self._codegen_error(
1754
2194
  expr_node,
@@ -1850,6 +2290,37 @@ class SecurityEmitter:
1850
2290
  resolving.remove(call_key)
1851
2291
  return resolved
1852
2292
 
2293
+ if (
2294
+ isinstance(expr_node, FuncCall)
2295
+ and isinstance(expr_node.callee, MemberAccess)
2296
+ and isinstance(expr_node.callee.object, Identifier)
2297
+ and expr_node.callee.object.name == "math"
2298
+ ):
2299
+ # A rolling/stateful math reducer (e.g. math.sum -> math::Sum) is
2300
+ # precomputed into a committed _secval_* by the security TA
2301
+ # machinery, exactly like a ta.* call. Return that committed value
2302
+ # instead of falling through to _build_security_math_call, whose
2303
+ # inline lowering only covers scalar math and emits a broken
2304
+ # "unsupported: math.<f>" 0.0 for a reducer. Scalar math
2305
+ # (abs/round/min/max/...) is not a TA site, so this is a no-op.
2306
+ math_site = self._get_ta_site(expr_node)
2307
+ if math_site is not None:
2308
+ math_idx = self._ta_index_by_site_id.get(id(math_site))
2309
+ math_sig = self._security_binding_stack_signature(helper_binding_stack)
2310
+ if math_idx is not None and (math_idx, math_sig) in ta_results:
2311
+ return ta_results[(math_idx, math_sig)]
2312
+ return self._build_security_math_call(
2313
+ sec_id,
2314
+ expr_node.callee.member,
2315
+ expr_node,
2316
+ ta_range,
2317
+ ta_results,
2318
+ resolving,
2319
+ security_mutable_names,
2320
+ helper_binding_stack,
2321
+ emitted_lines,
2322
+ )
2323
+
1853
2324
  site = self._get_ta_site(expr_node)
1854
2325
  if site:
1855
2326
  idx = self._ta_index_by_site_id.get(id(site))