@pineforge/codegen-pyodide 1.2.0 → 1.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.
Files changed (34) hide show
  1. package/LICENSE +19 -3
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +6 -5
  5. package/pineforge_codegen/analyzer/base.py +3 -3
  6. package/pineforge_codegen/analyzer/types.py +7 -3
  7. package/pineforge_codegen/codegen/base.py +36 -17
  8. package/pineforge_codegen/codegen/capabilities.py +143 -10
  9. package/pineforge_codegen/codegen/checked_settings.py +161 -4
  10. package/pineforge_codegen/codegen/collection_history.py +163 -12
  11. package/pineforge_codegen/codegen/emit_top.py +29 -13
  12. package/pineforge_codegen/codegen/helpers.py +25 -1
  13. package/pineforge_codegen/codegen/host_members.py +1 -0
  14. package/pineforge_codegen/codegen/input.py +23 -8
  15. package/pineforge_codegen/codegen/order_shapes.py +356 -0
  16. package/pineforge_codegen/codegen/run_stops.py +52 -0
  17. package/pineforge_codegen/codegen/security.py +66 -10
  18. package/pineforge_codegen/codegen/tables.py +144 -28
  19. package/pineforge_codegen/codegen/tv_number_format.py +9 -2
  20. package/pineforge_codegen/codegen/types.py +85 -4
  21. package/pineforge_codegen/codegen/visit_call.py +235 -55
  22. package/pineforge_codegen/codegen/visit_expr.py +40 -8
  23. package/pineforge_codegen/codegen/visit_stmt.py +5 -4
  24. package/pineforge_codegen/collection_history.py +122 -20
  25. package/pineforge_codegen/diagnostics_catalog.json +1 -0
  26. package/pineforge_codegen/external_requests.py +30 -9
  27. package/pineforge_codegen/matrix_overloads.py +7 -0
  28. package/pineforge_codegen/parser.py +7 -2
  29. package/pineforge_codegen/signatures.py +2 -2
  30. package/pineforge_codegen/support_checker.py +101 -10
  31. package/pineforge_codegen-1.4.0.tar.gz +0 -0
  32. package/release.json +2 -2
  33. package/tables.json +7 -3
  34. package/pineforge_codegen-1.2.0.tar.gz +0 -0
package/LICENSE CHANGED
@@ -1,6 +1,14 @@
1
- PineForge Source License 1.1
1
+ PineForge Source License 1.2
2
2
  ============================
3
3
 
4
+ Version 1.2, 2026-10-05. Compared with 1.1, two places change. In the
5
+ Personal Trading section, the last bullet now says that an account a
6
+ proprietary-trading firm or funded-trader program provides or allocates,
7
+ including a challenge, evaluation or simulated account, is not the
8
+ person's own account or capital. In the Commercial Use section, the
9
+ definition of investment capital now covers the capital in such an
10
+ account, real or simulated.
11
+
4
12
  Copyright 2025-2026 pineforge, LLC
5
13
 
6
14
  Required Notice: Copyright 2025-2026 pineforge, LLC (https://github.com/pineforge-4pass/pineforge-codegen-oss)
@@ -134,7 +142,12 @@ For Personal Trading:
134
142
  program or anyone else outside the person's household provides or
135
143
  allocates to the person to trade, and that the provider still owns or
136
144
  whose losses the provider bears, is not the person's own capital, even
137
- if the person shares in the profits.
145
+ if the person shares in the profits. An account that a
146
+ proprietary-trading firm or a funded-trader program provides or
147
+ allocates to the person to trade, including a challenge, evaluation or
148
+ simulated account, whether the capital in it is real or simulated, is
149
+ likewise not the person's own account and is not funded by the
150
+ person's own capital.
138
151
 
139
152
 
140
153
  ## Commercial Use
@@ -149,7 +162,10 @@ in each case whoever the capital belongs to and whether or not for a fee.
149
162
  trading, including capital belonging to any other person or entity, such as a
150
163
  friend or relative, clients or investors, an endowment, a pension or
151
164
  retirement fund, a sovereign or other public fund, or the treasury or reserves
152
- of a foundation or other organization.
165
+ of a foundation or other organization. The capital in an account that a
166
+ proprietary-trading firm or a funded-trader program provides or allocates
167
+ to a person or entity to trade, including a challenge, evaluation or
168
+ simulated account, is investment capital whether it is real or simulated.
153
169
 
154
170
  **Commercial Use** means any use of the software that is not a permitted
155
171
  purpose, other than distributing copies under the Distribution License
package/README.md CHANGED
@@ -19,7 +19,8 @@ Stable releases are on the `latest` dist-tag. A prerelease (for example
19
19
  - `pineforge_codegen/diagnostics_catalog.json` — the diagnostics catalog (since 1.2.0), also
20
20
  importable as `@pineforge/codegen-pyodide/diagnostics_catalog.json`; the repository's
21
21
  `docs/PUBLIC_CONTRACT.md` describes it.
22
- - `LICENSE` — the PineForge Source License 1.1 (since 1.2.0), which `package.json` names.
22
+ - `LICENSE` — the PineForge Source License (since 1.2.0), which `package.json` names:
23
+ version 1.1 in release 1.2.0, version 1.2 from release 1.3.0 on.
23
24
 
24
25
  ## Publishing (maintainers)
25
26
  A `v*` tag push, which the release workflow makes, publishes through npm OIDC Trusted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pineforge/codegen-pyodide",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -164,11 +164,12 @@ def transpile_full(pine_source: str, *, check_support: bool = True,
164
164
  - ``cpp``: the generated C++ source (identical to :func:`transpile`).
165
165
  - ``inputs``: a list of ``InputDef`` dicts (one per global-scope
166
166
  ``input(...)`` / ``input.*(...)`` call, inline calls included, in
167
- source order). Each has ``title`` / ``type`` /
168
- ``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
169
- (omitted when the corresponding signature argument is absent or
170
- references a non-const value); an ``input.symbol`` entry also has
171
- ``kind: "symbol"``. See :meth:`CodeGen.extract_input_manifest`.
167
+ source order). Each has ``title`` / ``type`` / ``default`` /
168
+ ``supported`` (the checked-settings receipt's flag) and optionally
169
+ ``min`` / ``max`` / ``step`` / ``options`` (omitted when the
170
+ corresponding signature argument is absent or is not a literal the
171
+ receipt holds); an ``input.symbol`` entry also has ``kind: "symbol"``.
172
+ See :meth:`CodeGen.extract_input_manifest`.
172
173
  - ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
173
174
  surfaced (e.g. ``initial_capital``, ``pyramiding``).
174
175
  - ``diagnostics``: the warnings (:class:`~pineforge_codegen.errors.Diagnostic`,
@@ -1758,7 +1758,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
1758
1758
  for collection_owner, collection_names in persistent_collections.items():
1759
1759
  if series_owner == collection_owner:
1760
1760
  continue
1761
- for exact_name in exact_series_names & collection_names:
1761
+ for exact_name in sorted(exact_series_names & collection_names):
1762
1762
  raw_name = next(
1763
1763
  (
1764
1764
  name for name in names
@@ -4999,7 +4999,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
4999
4999
  # ``exit_scope()`` can bind a same-named top-level collection
5000
5000
  # and falsely turn a scalar UDF return into an array.
5001
5001
  ret_spec = terminal_direct_return_spec
5002
- if ret_spec is not None and ret_spec.kind == "array":
5002
+ if ret_spec is not None and ret_spec.kind in ("array", "matrix"):
5003
5003
  self._func_return_type_specs[node.name] = ret_spec
5004
5004
  if (terminal_direct_return_spec is not None
5005
5005
  and terminal_direct_return_spec.kind == "map"):
@@ -5150,7 +5150,7 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
5150
5150
  ret_type = self._statement_value_type(node.body[-1], ret_type)
5151
5151
  if terminal_ret_expr is not None:
5152
5152
  terminal_spec = self._type_spec_from_expr(terminal_ret_expr)
5153
- if terminal_spec is not None and terminal_spec.kind == "map":
5153
+ if terminal_spec is not None and terminal_spec.kind in ("map", "matrix"):
5154
5154
  return_type_spec = terminal_spec
5155
5155
  if terminal_spec is not None and terminal_spec.kind == "udt":
5156
5156
  # Methods may return ``self`` or another UDT-typed
@@ -45,6 +45,7 @@ from ..ast_nodes import (
45
45
  SwitchStmt, TupleLiteral, UnaryOp,
46
46
  )
47
47
  from ..errors import Phase
48
+ from ..matrix_overloads import matrix_sum_has_rhs
48
49
  from ..symbols import PineType, TypeSpec, method_receiver_type_name
49
50
 
50
51
  # Drawing-objects-as-data type names (spec §4.1). Defined locally — the
@@ -537,8 +538,10 @@ class TypeHelper:
537
538
  else:
538
539
  elem = TypeSpec.primitive("float")
539
540
  return TypeSpec.matrix(elem)
540
- if ns == "matrix" and func in _MATRIX_RETURNING_METHODS:
541
- receiver = value.args[0] if value.args else value.kwargs.get("id")
541
+ if ns == "matrix" and (func in _MATRIX_RETURNING_METHODS or (
542
+ func == "sum" and matrix_sum_has_rhs(value, namespace=True))):
543
+ receiver = (value.args[0] if value.args else
544
+ value.kwargs.get("id1", value.kwargs.get("id")))
542
545
  receiver_spec = self._type_spec_from_expr(receiver)
543
546
  if receiver_spec is not None and receiver_spec.kind == "matrix":
544
547
  return receiver_spec
@@ -607,7 +610,8 @@ class TypeHelper:
607
610
  if func == "size":
608
611
  return TypeSpec.primitive("int")
609
612
  if recv_spec is not None and recv_spec.kind == "matrix":
610
- if func in _MATRIX_RETURNING_METHODS:
613
+ if func in _MATRIX_RETURNING_METHODS or (
614
+ func == "sum" and matrix_sum_has_rhs(value)):
611
615
  return recv_spec
612
616
  if func in ("row", "col"):
613
617
  return TypeSpec.array(recv_spec.element)
@@ -31,6 +31,7 @@ from ..symbols import PineType, TypeSpec, method_receiver_type_name
31
31
  from .. import signatures as sigs
32
32
  from ..errors import CompileError, Diagnostic, Level, Phase, SourceLocation
33
33
  from ..limits import TimeBudget, iter_ast_nodes
34
+ from ..matrix_overloads import matrix_sum_has_rhs
34
35
  from ..session_reads import emitted_session_reads
35
36
  from ..block_locals import block_declarations, decl_key
36
37
  from ..pine_spelling import (
@@ -240,6 +241,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
240
241
  def __init__(self, ctx: AnalyzerContext,
241
242
  budget: TimeBudget | None = None) -> None:
242
243
  self.ctx = ctx
244
+ self._order_shape_calls = {}
245
+ self._order_shape_host_reads: set[str] = set()
246
+ self._order_shape_constructor: list[str] = []
243
247
  self._budget = budget
244
248
  self._budget_visit_count = 0
245
249
  self._initialise_safe_names(ctx.ast)
@@ -1079,7 +1083,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
1079
1083
  # (index, signature) of each TA variant computed inline, and
1080
1084
  # (index, _SECURITY_THROUGH_GLOBAL) of a global's site a
1081
1085
  # multi-statement helper reads, which the prologue computes.
1082
- "inline_helper_ta_indices": sorted(inline_helper_ta_indices, key=repr),
1086
+ "inline_helper_ta_indices": inline_helper_ta_indices,
1083
1087
  "depends_on_mutable_globals": item.get("depends_on_mutable_globals", False),
1084
1088
  "mutable_globals": list(item.get("mutable_globals", [])),
1085
1089
  "is_lower_tf_array": bool(item.get("is_lower_tf_array", False)),
@@ -2153,6 +2157,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2153
2157
  # to the analyzer's default ``double`` and the emitted C++
2154
2158
  # fails to compile (``double = PineMatrix``).
2155
2159
  or fn in MATRIX_RETURNING_METHODS
2160
+ or (fn == "sum" and matrix_sum_has_rhs(expr, namespace=True))
2156
2161
  ):
2157
2162
  if fn == "new":
2158
2163
  targs = self._template_args_from_call(expr) if hasattr(expr, "annotations") else []
@@ -2210,7 +2215,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
2210
2215
  outer_callee = expr.callee
2211
2216
  if (
2212
2217
  isinstance(outer_callee, MemberAccess)
2213
- and outer_callee.member in MATRIX_RETURNING_METHODS
2218
+ and (outer_callee.member in MATRIX_RETURNING_METHODS or (
2219
+ outer_callee.member == "sum" and matrix_sum_has_rhs(expr)))
2214
2220
  ):
2215
2221
  recv_name2 = self._extract_receiver_name(expr)
2216
2222
  if recv_name2 is not None and recv_name2 in self._matrix_specs:
@@ -4490,7 +4496,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4490
4496
  self._prepare_lazy_source_clock_sites()
4491
4497
  self.block_locals_needing_names = self._block_locals_needing_names()
4492
4498
 
4493
- lines: list[str] = []
4499
+ from .order_shapes import HostReadLines
4500
+ lines: list[str] = HostReadLines(self._order_shape_host_reads)
4494
4501
 
4495
4502
  # Series<T> ctor-arg suffix from any max_bars_back directive (empty when
4496
4503
  # absent, so directive-free output is byte-identical to before).
@@ -4549,7 +4556,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4549
4556
  "",
4550
4557
  " Snapshot snapshot() {",
4551
4558
  " if (_pf_generation_ == std::numeric_limits<uint64_t>::max()) {",
4552
- ' throw std::overflow_error("UDT checkpoint generation exhausted");',
4559
+ ' _PF_ENGINE_INVARIANT("UDT checkpoint generation exhausted", std::overflow_error);',
4553
4560
  " }",
4554
4561
  " ++_pf_generation_;",
4555
4562
  " _pf_undo_.clear();",
@@ -4561,13 +4568,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4561
4568
  " bool empty() const { return _pf_undo_.empty(); }",
4562
4569
  " void record(uint64_t generation, std::function<void()> undo) {",
4563
4570
  " if (!_pf_active_ || generation != _pf_generation_) {",
4564
- ' throw std::runtime_error("invalid UDT undo generation");',
4571
+ ' _PF_ENGINE_INVARIANT("invalid UDT undo generation", std::runtime_error);',
4565
4572
  " }",
4566
4573
  " _pf_undo_.push_back(std::move(undo));",
4567
4574
  " }",
4568
4575
  " void restore(const Snapshot& snapshot) {",
4569
4576
  " if (!_pf_active_ || snapshot.generation != _pf_generation_) {",
4570
- ' throw std::runtime_error("invalid UDT coordinator checkpoint token");',
4577
+ ' _PF_ENGINE_INVARIANT("invalid UDT coordinator checkpoint token", std::runtime_error);',
4571
4578
  " }",
4572
4579
  " for (auto entry = _pf_undo_.rbegin();",
4573
4580
  " entry != _pf_undo_.rend(); ++entry) (*entry)();",
@@ -4591,13 +4598,13 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4591
4598
  "",
4592
4599
  " void capture(std::size_t index) {",
4593
4600
  " if (!_pf_checkpoint_active_) return;",
4594
- " auto& slot = _pf_records_.at(index);",
4601
+ " auto& slot = _PF_INVARIANT_AT(_pf_records_, index);",
4595
4602
  " if (slot.logged_generation",
4596
4603
  " == _pf_checkpoint_generation_) return;",
4597
4604
  " auto snapshot = _PFRecordTraits::take(slot.value);",
4598
4605
  " _pf_coordinator_->record(_pf_checkpoint_generation_,",
4599
4606
  " [this, index, snapshot = std::move(snapshot)]() mutable {",
4600
- " auto& restore_slot = _pf_records_.at(index);",
4607
+ " auto& restore_slot = _PF_INVARIANT_AT(_pf_records_, index);",
4601
4608
  " _PFRecordTraits::restore(restore_slot.value, snapshot);",
4602
4609
  " restore_slot.logged_generation = 0;",
4603
4610
  " });",
@@ -4613,7 +4620,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4613
4620
  f" {self._udt_undo_coordinator_cpp_name}* coordinator)",
4614
4621
  " : _pf_coordinator_(coordinator) {",
4615
4622
  " if (!_pf_coordinator_)",
4616
- ' throw std::invalid_argument("UDT arena requires undo coordinator");',
4623
+ ' _PF_ENGINE_INVARIANT("UDT arena requires undo coordinator", std::invalid_argument);',
4617
4624
  " }",
4618
4625
  f" {self._udt_arena_template_cpp_name}(",
4619
4626
  f" const {self._udt_arena_template_cpp_name}&) = delete;",
@@ -4635,7 +4642,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4635
4642
  " _PFHandle create(_PFRecord value) {",
4636
4643
  " if (_pf_records_.size() > static_cast<std::size_t>(",
4637
4644
  " std::numeric_limits<int32_t>::max())) {",
4638
- ' throw std::length_error("UDT object-ID capacity exceeded");',
4645
+ ' _PF_LIMIT_STOP("udt_objects", 2147483647LL, "UDT object-ID capacity exceeded");',
4639
4646
  " }",
4640
4647
  " const auto id = static_cast<int32_t>(_pf_records_.size());",
4641
4648
  " _pf_records_.push_back(_PFSlot{std::move(value), 0});",
@@ -4648,7 +4655,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4648
4655
  " _PFRecord& get(_PFHandle value) {",
4649
4656
  " if (value.__pf_id < 0",
4650
4657
  " || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
4651
- ' throw std::runtime_error("UDT access on na or invalid object ID");',
4658
+ ' _PF_NA_STOP("udt_object", "UDT access on na or invalid object ID");',
4652
4659
  " }",
4653
4660
  " const auto index = static_cast<std::size_t>(value.__pf_id);",
4654
4661
  " capture(index);",
@@ -4657,7 +4664,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4657
4664
  " const _PFRecord& get(_PFHandle value) const {",
4658
4665
  " if (value.__pf_id < 0",
4659
4666
  " || static_cast<std::size_t>(value.__pf_id) >= _pf_records_.size()) {",
4660
- ' throw std::runtime_error("UDT access on na or invalid object ID");',
4667
+ ' _PF_NA_STOP("udt_object", "UDT access on na or invalid object ID");',
4661
4668
  " }",
4662
4669
  " return _pf_records_[static_cast<std::size_t>(value.__pf_id)].value;",
4663
4670
  " }",
@@ -4667,14 +4674,14 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4667
4674
  " std::size_t size() const { return _pf_records_.size(); }",
4668
4675
  " _PFRecord& record_at(std::size_t index) {",
4669
4676
  " capture(index);",
4670
- " return _pf_records_.at(index).value;",
4677
+ " return _PF_INVARIANT_AT(_pf_records_, index).value;",
4671
4678
  " }",
4672
4679
  " const _PFRecord& record_at(std::size_t index) const {",
4673
- " return _pf_records_.at(index).value;",
4680
+ " return _PF_INVARIANT_AT(_pf_records_, index).value;",
4674
4681
  " }",
4675
4682
  " Snapshot snapshot() {",
4676
4683
  " if (!_pf_coordinator_->active()) {",
4677
- ' throw std::runtime_error("UDT coordinator checkpoint is not active");',
4684
+ ' _PF_ENGINE_INVARIANT("UDT coordinator checkpoint is not active", std::runtime_error);',
4678
4685
  " }",
4679
4686
  " _pf_checkpoint_generation_ = _pf_coordinator_->generation();",
4680
4687
  " _pf_checkpoint_size_ = _pf_records_.size();",
@@ -4689,7 +4696,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4689
4696
  " || snapshot.size != _pf_checkpoint_size_",
4690
4697
  " || _pf_records_.size() < snapshot.size",
4691
4698
  " || !_pf_coordinator_->empty()) {",
4692
- ' throw std::runtime_error("invalid UDT checkpoint token");',
4699
+ ' _PF_ENGINE_INVARIANT("invalid UDT checkpoint token", std::runtime_error);',
4693
4700
  " }",
4694
4701
  " _pf_records_.resize(snapshot.size);",
4695
4702
  " }",
@@ -4980,6 +4987,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
4980
4987
  if name in self._global_collection_types
4981
4988
  else self._callable_var_collection_spec(name)
4982
4989
  )
4990
+ history_value_cpp = self._array_history_value_cpp_type(name)
4991
+ if history_value_cpp is not None:
4992
+ lines.append(f" {history_value_cpp} {safe};")
4993
+ continue
4983
4994
  if callable_collection_spec is not None:
4984
4995
  lines.append(
4985
4996
  f" {self._type_spec_to_cpp(callable_collection_spec)} {safe};"
@@ -5178,7 +5189,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5178
5189
  seen_global.add(name)
5179
5190
  safe = self._safe_name(name)
5180
5191
 
5181
- if name in self._matrix_specs:
5192
+ history_value_cpp = self._array_history_value_cpp_type(name)
5193
+ if history_value_cpp is not None:
5194
+ lines.append(f" {history_value_cpp} {safe};")
5195
+ elif name in self._matrix_specs:
5182
5196
  lines.append(f" {self._type_spec_to_cpp(self._matrix_specs[name])} {safe};")
5183
5197
  elif name in self._array_vars:
5184
5198
  lines.append(f" {self._type_spec_to_cpp(self._array_spec_for_name(name))} {safe};")
@@ -5450,7 +5464,12 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
5450
5464
  if self._uses_session_market:
5451
5465
  lines.insert(_session_market_member_at, SESSION_MARKET_MEMBER)
5452
5466
  lines.insert(_session_market_at, SESSION_MARKET_CPP)
5467
+ self._capabilities_export_at += 2
5453
5468
 
5469
+ from .capabilities import emit_capabilities_exports
5470
+ exports: list[str] = []
5471
+ emit_capabilities_exports(self, exports)
5472
+ lines[self._capabilities_export_at:self._capabilities_export_at] = exports
5454
5473
  cpp = "\n".join(lines)
5455
5474
  self._settle_session_reads(cpp)
5456
5475
  return cpp
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  import math
5
5
 
6
- from ..ast_nodes import BoolLiteral, FuncCall, Identifier, MemberAccess, NumberLiteral, StrategyDecl, StringLiteral, UnaryOp, VarDecl
6
+ from ..ast_nodes import BoolLiteral, FuncCall, Identifier, MemberAccess, NumberLiteral, StrategyDecl, StringLiteral, Subscript, UnaryOp, VarDecl
7
7
  from ..external_requests import CAPABILITY_UNPINNED_ANNOTATION
8
8
  from ..limits import iter_ast_nodes
9
9
 
@@ -34,6 +34,28 @@ DECLARATION_DEFAULTS = {
34
34
  "calc_on_every_history_tick": False,
35
35
  }
36
36
 
37
+ MODELED_ORDER_ARGUMENTS = {
38
+ "strategy.entry": frozenset(("id", "direction", "limit", "stop")),
39
+ "strategy.exit": frozenset(("id", "from_entry", "limit", "stop")),
40
+ "strategy.close": frozenset(("id", "immediately")),
41
+ "strategy.close_all": frozenset(("immediately",)),
42
+ }
43
+ READ_ONLY_STRATEGY_CALLS = frozenset(
44
+ f"strategy.{namespace}.{accessor}"
45
+ for namespace in ("opentrades", "closedtrades")
46
+ for accessor in ("entry_id", "entry_price", "entry_time", "entry_bar_index", "size",
47
+ "profit", "profit_percent", "commission", "max_runup", "max_runup_percent",
48
+ "max_drawdown", "max_drawdown_percent", "exit_id", "exit_price",
49
+ "exit_time", "exit_bar_index", "entry_comment", "exit_comment")
50
+ ) | frozenset(("strategy.convert_to_account", "strategy.convert_to_symbol"))
51
+ ORDER_SETTINGS = frozenset(("pyramiding", "default_qty_type", "default_qty_value", "initial_capital",
52
+ "slippage", "commission_type", "commission_value", "close_entries_rule",
53
+ "margin_long", "margin_short", "risk_free_rate"))
54
+ PROVEN_BRACKET_SETTINGS = {"default_qty_type": "strategy.fixed", "default_qty_value": 1,
55
+ "commission_type": "strategy.commission.percent", "commission_value": 0}
56
+ PROVEN_SLIPPED_SETTINGS = {"default_qty_type": "strategy.percent_of_equity",
57
+ "default_qty_value": 100, "slippage": 15}
58
+
37
59
 
38
60
  def _expression(node):
39
61
  if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
@@ -44,6 +66,8 @@ def _expression(node):
44
66
  return f"{_expression(node.object)}.{node.member}"
45
67
  if isinstance(node, FuncCall):
46
68
  return f"{_expression(node.callee)}({','.join(str(_expression(arg)) for arg in node.args)})"
69
+ if isinstance(node, Subscript):
70
+ return f"{_expression(node.object)}[{_expression(node.index)}]"
47
71
  return None
48
72
 
49
73
 
@@ -61,13 +85,57 @@ def _literal(node):
61
85
  return _expression(node), False
62
86
 
63
87
 
64
- def capabilities_document(emitter) -> str:
88
+ def _order_arguments(node):
89
+ name = _expression(node.callee)
90
+ positional = {
91
+ "strategy.entry": ("id", "direction", "qty", "limit", "stop", "oca_name", "oca_type", "comment", "alert_message", "disable_alert"),
92
+ "strategy.exit": ("id", "from_entry", "qty", "qty_percent", "profit", "limit", "loss", "stop", "trail_price", "trail_points", "trail_offset", "oca_name", "comment", "alert_message", "alert_profit", "alert_loss", "alert_trailing", "disable_alert"),
93
+ "strategy.close": ("id", "comment", "qty", "qty_percent", "alert_message", "immediately", "disable_alert"),
94
+ "strategy.close_all": ("comment", "alert_message", "immediately", "disable_alert"),
95
+ }.get(name, ())
96
+ arguments = {positional[index] if index < len(positional) else f"positional_{index}": argument
97
+ for index, argument in enumerate(node.args)}
98
+ if arguments.keys() & node.kwargs.keys():
99
+ arguments["duplicate_argument"] = None
100
+ arguments.update(node.kwargs)
101
+ return arguments
102
+
103
+
104
+ def _order_shape(node, short_ids):
105
+ name = _expression(node.callee)
106
+ arguments = _order_arguments(node)
107
+ if name not in MODELED_ORDER_ARGUMENTS or not set(arguments) <= MODELED_ORDER_ARGUMENTS[name]:
108
+ return name + (" (unproven exit terms)" if name == "strategy.exit" else " (unproven order shape)")
109
+ if name != "strategy.close_all" and not isinstance(arguments.get("id"), StringLiteral):
110
+ return name + " (unproven order id)"
111
+ if name == "strategy.entry" and _expression(arguments.get("direction")) in ("strategy.long", "strategy.short"):
112
+ priced = {key for key in ("limit", "stop") if key in arguments}
113
+ if len(priced) < 2 and not (set(arguments) & {"oca_name", "oca_type"}):
114
+ return "entry:" + (next(iter(priced)) if priced else "market")
115
+ if name == "strategy.exit":
116
+ if (set(arguments) & {"profit", "loss", "trail_price", "trail_points", "trail_offset", "qty", "qty_percent"}):
117
+ return name + " (unproven exit terms)"
118
+ if ("limit" in arguments and "stop" in arguments
119
+ and isinstance(arguments.get("from_entry"), StringLiteral)
120
+ and _expression(arguments["from_entry"]) in short_ids):
121
+ return "exit:short_bracket"
122
+ if name in ("strategy.close", "strategy.close_all"):
123
+ immediate = arguments.get("immediately")
124
+ if immediate is None or isinstance(immediate, BoolLiteral) and not immediate.value:
125
+ if not (set(arguments) & {"qty", "qty_percent"}):
126
+ return "close:market"
127
+ return name + " (unproven order shape)"
128
+
129
+
130
+ def capabilities_documents(emitter) -> tuple[str, str]:
65
131
  declarations = dict(DECLARATION_DEFAULTS)
66
132
  unresolved = []
67
133
  requests = []
68
134
  lower_symbols = {}
69
135
  intrabar = False
70
136
  recorded = False
137
+ order_nodes = []
138
+ order_settings = {}
71
139
  for node, _depth in iter_ast_nodes(emitter.ctx.ast):
72
140
  if isinstance(node, StrategyDecl):
73
141
  arguments = {STRATEGY_PARAMETERS[index] if index < len(STRATEGY_PARAMETERS)
@@ -75,6 +143,8 @@ def capabilities_document(emitter) -> str:
75
143
  for index, argument in enumerate(node.args)}
76
144
  unresolved.extend(arguments.keys() & node.kwargs.keys())
77
145
  arguments.update(node.kwargs)
146
+ order_settings.update({name: _literal(argument)[0] for name, argument in arguments.items()
147
+ if name in ORDER_SETTINGS})
78
148
  for name, argument in arguments.items():
79
149
  value, valid = _literal(argument)
80
150
  default = DECLARATION_DEFAULTS.get(name)
@@ -92,6 +162,9 @@ def capabilities_document(emitter) -> str:
92
162
  intrabar |= node.is_varip
93
163
  elif isinstance(node, FuncCall):
94
164
  name, namespace = emitter._resolve_callee(node.callee)
165
+ qualified = _expression(node.callee) or ""
166
+ if qualified.startswith("strategy.") and qualified not in READ_ONLY_STRATEGY_CALLS:
167
+ order_nodes.append(node)
95
168
  if namespace == "request" and name == "security_lower_tf":
96
169
  expression = node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
97
170
  lower_symbols[id(expression)] = node.args[0] if node.args else node.kwargs.get("symbol")
@@ -118,22 +191,27 @@ def capabilities_document(emitter) -> str:
118
191
  unresolved.append(f"{_expression(node)} (stream endpoint is not batch-equivalent)")
119
192
 
120
193
  auxiliary = False
194
+ confirmed_requests = []
195
+ lowering_sites = {site["sec_id"]: site for site in emitter._security_eval_info}
121
196
  for site in emitter._security_calls:
197
+ lowering = lowering_sites[site["sec_id"]]
122
198
  symbol_node = site.get("symbol_node")
123
199
  if site.get("is_lower_tf_array"):
124
200
  symbol_node = lower_symbols.get(id(site.get("expr_node")), symbol_node)
125
201
  if isinstance(symbol_node, FuncCall) and site.get("heikinashi") and symbol_node.args:
126
202
  symbol_node = symbol_node.args[0]
127
203
  symbol = _expression(symbol_node) if symbol_node is not None else "syminfo.tickerid"
128
- timeframe = _expression(site.get("tf_node"))
204
+ timeframe = lowering.get("tf")
205
+ if timeframe is None:
206
+ timeframe = "timeframe.period" if lowering.get("tf_expr") == "script_tf_" else _expression(site.get("tf_node"))
129
207
  lookahead = _expression(site.get("lookahead_node")) or "barmerge.lookahead_off"
130
208
  gaps = _expression(site.get("gaps_node")) or "barmerge.gaps_off"
131
209
  if isinstance(symbol_node, Identifier) and symbol_node.name not in emitter._global_mutable_infos:
132
210
  symbol = _expression((getattr(emitter.ctx, "global_expr_map", {}) or {}).get(
133
211
  symbol_node.name, symbol_node))
134
- feed = "chart" if symbol in ("syminfo.tickerid", "syminfo.ticker", "") else "auxiliary"
212
+ feed = "auxiliary" if lowering.get("foreign") else "chart"
135
213
  auxiliary |= feed == "auxiliary"
136
- if not isinstance(site.get("tf_node"), StringLiteral) and timeframe != "timeframe.period":
214
+ if lowering.get("tf") is None and timeframe != "timeframe.period":
137
215
  unresolved.append(f"request.security[{site['sec_id']}].timeframe")
138
216
  if lookahead not in ("barmerge.lookahead_off", "barmerge.lookahead_on"):
139
217
  unresolved.append(f"request.security[{site['sec_id']}].lookahead")
@@ -143,9 +221,14 @@ def capabilities_document(emitter) -> str:
143
221
  "timeframe": timeframe,
144
222
  "lookahead": lookahead,
145
223
  "gaps": gaps,
146
- "heikinashi": bool(site.get("heikinashi")),
224
+ "heikinashi": bool(lowering.get("heikinashi")),
147
225
  "feed": feed,
148
226
  })
227
+ expression = _expression(site.get("expr_node"))
228
+ if expression in ("close", "close[1]", "ta.sma(close,4)", "ta.ema(close,3)") and "close" in (
229
+ getattr(emitter.ctx, "global_expr_map", {}) or {}):
230
+ unresolved.append("request.security.expression (user-bound close)")
231
+ confirmed_requests.append({**requests[-1], "expression": expression})
149
232
  for index, site in enumerate((emitter.ctx.ast.annotations or {}).get(CAPABILITY_UNPINNED_ANNOTATION, ())):
150
233
  requests.append({
151
234
  "function": site["function"],
@@ -166,13 +249,46 @@ def capabilities_document(emitter) -> str:
166
249
  "historical_probe_overrides": False,
167
250
  "intrabar_persistence": intrabar,
168
251
  }
169
- return json.dumps({"version": 1, "declarations": declarations, "requests": requests,
170
- "requirements": requirements, "unresolved": sorted(set(unresolved))},
171
- sort_keys=True, separators=(",", ":"), allow_nan=False)
252
+ short_ids = set()
253
+ conflicting_ids = set()
254
+ dynamic_entry_id = False
255
+ for node in order_nodes:
256
+ if _expression(node.callee) != "strategy.entry":
257
+ continue
258
+ arguments = _order_arguments(node)
259
+ identifier = arguments.get("id")
260
+ if not isinstance(identifier, StringLiteral):
261
+ dynamic_entry_id = True
262
+ elif _expression(arguments.get("direction")) == "strategy.short":
263
+ short_ids.add(identifier.value)
264
+ else:
265
+ conflicting_ids.add(identifier.value)
266
+ if dynamic_entry_id or conflicting_ids:
267
+ short_ids.clear()
268
+ orders = set(_order_shape(node, short_ids - conflicting_ids) for node in order_nodes)
269
+ if declarations["process_orders_on_close"]:
270
+ bracket = orders == {"entry:market", "exit:short_bracket"}
271
+ proven = (not order_settings and not bracket) or (
272
+ bracket and order_settings == PROVEN_BRACKET_SETTINGS) or (
273
+ orders == {"entry:market"} and order_settings == PROVEN_SLIPPED_SETTINGS
274
+ and not confirmed_requests and not intrabar)
275
+ if not proven:
276
+ orders.add("strategy() (unproven POOC sizing, slippage or account settings)")
277
+ confirmed = {"version": 1, "requests": confirmed_requests,
278
+ "orders": sorted(orders),
279
+ "intrabar_persistence": intrabar}
280
+ encode = lambda value: json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
281
+ return (encode({"version": 1, "declarations": declarations, "requests": requests,
282
+ "requirements": requirements, "unresolved": sorted(set(unresolved))}), encode(confirmed))
283
+
284
+
285
+ def capabilities_document(emitter) -> str:
286
+ return capabilities_documents(emitter)[0]
172
287
 
173
288
 
174
289
  def emit_capabilities_exports(emitter, lines: list[str]) -> None:
175
- literal = json.dumps(capabilities_document(emitter))
290
+ legacy, confirmed = capabilities_documents(emitter)
291
+ literal = json.dumps(legacy)
176
292
  lines.extend([
177
293
  "#ifdef PF_SETTINGS_API_VERSION",
178
294
  "#ifdef PF_CAPABILITIES_API_VERSION",
@@ -182,5 +298,22 @@ def emit_capabilities_exports(emitter, lines: list[str]) -> None:
182
298
  " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
183
299
  ' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
184
300
  f" ::pineforge::checked_settings::receipt({literal}, json, capacity, required);",
301
+ " });", " }",
302
+ " uint32_t strategy_confirmed_bar_api_version(void) { return 1u; }",
303
+ " int strategy_confirmed_bar_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
304
+ " if (required) *required = 0;",
305
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
306
+ ' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
307
+ f" ::pineforge::checked_settings::receipt({json.dumps(confirmed)}, json, capacity, required);",
185
308
  " });", " }", "#endif", "#endif",
186
309
  ])
310
+ from .order_shapes import order_shapes_document
311
+ lines[-2:-2] = [
312
+ " uint32_t strategy_order_shapes_api_version(void) { return 1u; }",
313
+ " int strategy_order_shapes_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
314
+ " if (required) *required = 0;",
315
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
316
+ ' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
317
+ f" ::pineforge::checked_settings::receipt({json.dumps(order_shapes_document(emitter))}, json, capacity, required);",
318
+ " });", " }",
319
+ ]