@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.
- package/LICENSE +19 -3
- package/README.md +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +6 -5
- package/pineforge_codegen/analyzer/base.py +3 -3
- package/pineforge_codegen/analyzer/types.py +7 -3
- package/pineforge_codegen/codegen/base.py +36 -17
- package/pineforge_codegen/codegen/capabilities.py +143 -10
- package/pineforge_codegen/codegen/checked_settings.py +161 -4
- package/pineforge_codegen/codegen/collection_history.py +163 -12
- package/pineforge_codegen/codegen/emit_top.py +29 -13
- package/pineforge_codegen/codegen/helpers.py +25 -1
- package/pineforge_codegen/codegen/host_members.py +1 -0
- package/pineforge_codegen/codegen/input.py +23 -8
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/run_stops.py +52 -0
- package/pineforge_codegen/codegen/security.py +66 -10
- package/pineforge_codegen/codegen/tables.py +144 -28
- package/pineforge_codegen/codegen/tv_number_format.py +9 -2
- package/pineforge_codegen/codegen/types.py +85 -4
- package/pineforge_codegen/codegen/visit_call.py +235 -55
- package/pineforge_codegen/codegen/visit_expr.py +40 -8
- package/pineforge_codegen/codegen/visit_stmt.py +5 -4
- package/pineforge_codegen/collection_history.py +122 -20
- package/pineforge_codegen/diagnostics_catalog.json +1 -0
- package/pineforge_codegen/external_requests.py +30 -9
- package/pineforge_codegen/matrix_overloads.py +7 -0
- package/pineforge_codegen/parser.py +7 -2
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +101 -10
- package/pineforge_codegen-1.4.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -3
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
PineForge Source License 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
|
|
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.
|
|
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
|
-
``
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
``kind: "symbol"``.
|
|
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
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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_
|
|
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_
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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_
|
|
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_
|
|
4680
|
+
" return _PF_INVARIANT_AT(_pf_records_, index).value;",
|
|
4674
4681
|
" }",
|
|
4675
4682
|
" Snapshot snapshot() {",
|
|
4676
4683
|
" if (!_pf_coordinator_->active()) {",
|
|
4677
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 = "
|
|
212
|
+
feed = "auxiliary" if lowering.get("foreign") else "chart"
|
|
135
213
|
auxiliary |= feed == "auxiliary"
|
|
136
|
-
if
|
|
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(
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
+
]
|