@pineforge/codegen-pyodide 1.1.0 → 1.2.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 +245 -0
- package/README.md +4 -0
- package/glue.py +4 -0
- package/package.json +6 -4
- package/pineforge_codegen/__init__.py +1 -0
- package/pineforge_codegen/codegen/base.py +38 -1
- package/pineforge_codegen/codegen/capabilities.py +186 -0
- package/pineforge_codegen/codegen/checked_settings.py +16 -3
- package/pineforge_codegen/codegen/emit_top.py +2 -0
- package/pineforge_codegen/codegen/helpers.py +1 -0
- package/pineforge_codegen/diagnostic_codes.py +341 -0
- package/pineforge_codegen/diagnostics_catalog.json +631 -0
- package/pineforge_codegen/errors.py +23 -0
- package/pineforge_codegen/external_requests.py +16 -0
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +4 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
PineForge Source License 1.1
|
|
2
|
+
============================
|
|
3
|
+
|
|
4
|
+
Copyright 2025-2026 pineforge, LLC
|
|
5
|
+
|
|
6
|
+
Required Notice: Copyright 2025-2026 pineforge, LLC (https://github.com/pineforge-4pass/pineforge-codegen-oss)
|
|
7
|
+
|
|
8
|
+
The software is free of charge for the permitted purposes below, including
|
|
9
|
+
Personal Trading. Commercial Use, defined below, needs a separate commercial
|
|
10
|
+
license from the licensor.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Acceptance
|
|
14
|
+
|
|
15
|
+
In order to get any license under these terms, you must agree to them as both
|
|
16
|
+
strict obligations and conditions to all your licenses.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Copyright License
|
|
20
|
+
|
|
21
|
+
The licensor grants you a copyright license for the software to do everything
|
|
22
|
+
you might do with the software that would otherwise infringe the licensor's
|
|
23
|
+
copyright in it for any permitted purpose. However, you may only distribute
|
|
24
|
+
the software according to the Distribution License section, and make changes
|
|
25
|
+
or new works based on the software according to the Changes and New Works
|
|
26
|
+
License section.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Distribution License
|
|
30
|
+
|
|
31
|
+
The licensor grants you an additional copyright license to distribute copies
|
|
32
|
+
of the software. Your license to distribute covers distributing the software
|
|
33
|
+
with changes and new works permitted by the Changes and New Works License
|
|
34
|
+
section. Unless it is a permitted purpose, distributing the software, changed
|
|
35
|
+
or not, embedded in or bundled with a product or service made available to
|
|
36
|
+
others is Commercial Use under (3) of the Commercial Use section, which your
|
|
37
|
+
license to distribute does not cover. Distributing copies under this section
|
|
38
|
+
is not Commercial Use, and it gives the people who get them no license beyond
|
|
39
|
+
these terms.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Notices
|
|
43
|
+
|
|
44
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
45
|
+
you also gets a copy of these terms or a link to them, as well as copies of
|
|
46
|
+
any plain-text lines beginning with `Required Notice:` that the licensor
|
|
47
|
+
provided with the software. For example:
|
|
48
|
+
|
|
49
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Changes and New Works License
|
|
53
|
+
|
|
54
|
+
The licensor grants you an additional copyright license to make changes and
|
|
55
|
+
new works based on the software for any permitted purpose.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Patent License
|
|
59
|
+
|
|
60
|
+
The licensor grants you a patent license for the software that covers patent
|
|
61
|
+
claims the licensor can license, or becomes able to license, that you would
|
|
62
|
+
infringe by using the software for a permitted purpose.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## Permitted Purposes
|
|
66
|
+
|
|
67
|
+
Each of the following is a permitted purpose:
|
|
68
|
+
|
|
69
|
+
- **Noncommercial purposes.** Any noncommercial purpose.
|
|
70
|
+
|
|
71
|
+
- **Personal uses.** Personal use for research, experiment, and testing for
|
|
72
|
+
the benefit of public knowledge, personal study, private entertainment,
|
|
73
|
+
hobby projects, amateur pursuits, or religious observance, without any
|
|
74
|
+
anticipated commercial application.
|
|
75
|
+
|
|
76
|
+
- **Noncommercial organizations.** Use by any charitable organization,
|
|
77
|
+
educational institution, public research organization, public safety or
|
|
78
|
+
health organization, environmental protection organization, or government
|
|
79
|
+
institution for its teaching, research and other operations, regardless of
|
|
80
|
+
the source of funding or obligations resulting from the funding.
|
|
81
|
+
|
|
82
|
+
- **Personal Trading**, as defined in the next section.
|
|
83
|
+
|
|
84
|
+
Investment Management, defined in the Commercial Use section, is never a
|
|
85
|
+
permitted purpose, except as Personal Trading. That is so even when it is done
|
|
86
|
+
by a noncommercial organization or for a noncommercial purpose.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Personal Trading
|
|
90
|
+
|
|
91
|
+
**Personal Trading** means use of the software by a natural person to do any
|
|
92
|
+
of the following, solely for trades that meet conditions (a) and (b) below:
|
|
93
|
+
research, develop or backtest trading strategies, or execute trades in
|
|
94
|
+
securities, derivatives, currencies, crypto-assets, commodities or other
|
|
95
|
+
financial instruments.
|
|
96
|
+
|
|
97
|
+
(a) The trades are, or will be, placed for that person's own account.
|
|
98
|
+
|
|
99
|
+
(b) The trades are, or will be, funded solely by that person's own
|
|
100
|
+
capital.
|
|
101
|
+
|
|
102
|
+
For Personal Trading:
|
|
103
|
+
|
|
104
|
+
- A person's **household** is that person, their spouse or domestic
|
|
105
|
+
partner, and their dependants.
|
|
106
|
+
|
|
107
|
+
- A person's **own account** includes an account held by the person alone
|
|
108
|
+
or jointly with members of their household; an account of a member of
|
|
109
|
+
their household; and a retirement, pension, education-savings or other
|
|
110
|
+
tax-advantaged account, or an account of a revocable trust, held for the
|
|
111
|
+
benefit of the person or members of their household.
|
|
112
|
+
|
|
113
|
+
- A person's **own capital** includes capital belonging to members of
|
|
114
|
+
their household, and capital borrowed in the ordinary course from a
|
|
115
|
+
broker, bank or other lender, such as margin, leverage or credit on a
|
|
116
|
+
trading account, for which the person or their household is liable.
|
|
117
|
+
Money the person has received outright, such as pay, a gift or an
|
|
118
|
+
inheritance, is the person's own capital.
|
|
119
|
+
|
|
120
|
+
- Trading in the person's own name, for their own account and with their
|
|
121
|
+
own capital, is Personal Trading even when it is the person's business
|
|
122
|
+
or livelihood.
|
|
123
|
+
|
|
124
|
+
- Publishing or teaching about the person's own Personal Trading,
|
|
125
|
+
including its results, is part of Personal Trading even when the person
|
|
126
|
+
is paid for it, as long as they do not give Output to others.
|
|
127
|
+
|
|
128
|
+
- An account of a company, partnership, fund, trust or other
|
|
129
|
+
organization, other than an account described under own account above,
|
|
130
|
+
is not the person's own account, even if the person wholly owns or
|
|
131
|
+
controls it.
|
|
132
|
+
|
|
133
|
+
- Capital that an employer, a proprietary-trading firm, a funded-trader
|
|
134
|
+
program or anyone else outside the person's household provides or
|
|
135
|
+
allocates to the person to trade, and that the provider still owns or
|
|
136
|
+
whose losses the provider bears, is not the person's own capital, even
|
|
137
|
+
if the person shares in the profits.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
## Commercial Use
|
|
141
|
+
|
|
142
|
+
**Investment Management** means using the software to manage, advise on, or
|
|
143
|
+
trade investment capital; to research, develop or backtest strategies for
|
|
144
|
+
investment capital that you, or anyone you do the work for, manage, advise on
|
|
145
|
+
or trade; or to operate the software for others who do any of these things;
|
|
146
|
+
in each case whoever the capital belongs to and whether or not for a fee.
|
|
147
|
+
|
|
148
|
+
**Investment capital** means any money or other assets held for investment or
|
|
149
|
+
trading, including capital belonging to any other person or entity, such as a
|
|
150
|
+
friend or relative, clients or investors, an endowment, a pension or
|
|
151
|
+
retirement fund, a sovereign or other public fund, or the treasury or reserves
|
|
152
|
+
of a foundation or other organization.
|
|
153
|
+
|
|
154
|
+
**Commercial Use** means any use of the software that is not a permitted
|
|
155
|
+
purpose, other than distributing copies under the Distribution License
|
|
156
|
+
section. Investment Management is Commercial Use for every individual and
|
|
157
|
+
every organization, including the noncommercial organizations listed under
|
|
158
|
+
Permitted Purposes, whatever its purpose, unless it is Personal Trading.
|
|
159
|
+
Unless it is a permitted purpose, each of the following is also Commercial
|
|
160
|
+
Use:
|
|
161
|
+
|
|
162
|
+
(1) managing, advising on, or trading capital belonging to any other person
|
|
163
|
+
or entity, whether or not for a fee;
|
|
164
|
+
|
|
165
|
+
(2) use by, for, or on behalf of any company, fund, partnership, or other
|
|
166
|
+
organization, including use by an individual in the course of work for
|
|
167
|
+
such an organization;
|
|
168
|
+
|
|
169
|
+
(3) embedding the software or its Output, changed or not, into any product
|
|
170
|
+
or service made available to others, or distributing either bundled
|
|
171
|
+
with such a product or service, including using the software to
|
|
172
|
+
generate Output for such a product or service; and
|
|
173
|
+
|
|
174
|
+
(4) operating any hosted, software-as-a-service, or otherwise public-facing
|
|
175
|
+
service through which others can run the software or receive its
|
|
176
|
+
Output.
|
|
177
|
+
|
|
178
|
+
Commercial Use requires a separate commercial license from the licensor. To
|
|
179
|
+
obtain one, contact: enterprise@pineforge.dev
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## Output
|
|
183
|
+
|
|
184
|
+
**Output** means code that the software generates, such as the C++ source
|
|
185
|
+
code it generates from PineScript, and any program or library built from that
|
|
186
|
+
code. Backtest results, charts, reports and trade signals are not Output.
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## Fair Use
|
|
190
|
+
|
|
191
|
+
You may have "fair use" rights for the software under the law. These terms do
|
|
192
|
+
not limit them.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## No Other Rights
|
|
196
|
+
|
|
197
|
+
These terms do not allow you to sublicense or transfer any of your licenses
|
|
198
|
+
to anyone else, or prevent the licensor from granting licenses to anyone
|
|
199
|
+
else. These terms do not imply any other licenses.
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
## Patent Defense
|
|
203
|
+
|
|
204
|
+
If you make any written claim that the software infringes or contributes to
|
|
205
|
+
infringement of any patent, your patent license for the software granted
|
|
206
|
+
under these terms ends immediately. If your company makes such a claim, your
|
|
207
|
+
patent license ends immediately for work on behalf of your company.
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## Violations
|
|
211
|
+
|
|
212
|
+
The first time you are notified in writing that you have violated any of
|
|
213
|
+
these terms, or done anything with the software not covered by your licenses,
|
|
214
|
+
your licenses can nonetheless continue if you come into full compliance with
|
|
215
|
+
these terms, and take practical steps to correct past violations, within 32
|
|
216
|
+
days of receiving notice. Otherwise, all your licenses end immediately.
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## No Liability
|
|
220
|
+
|
|
221
|
+
***As far as the law allows, the software comes as is, without any warranty
|
|
222
|
+
or condition, and the licensor will not be liable to you for any damages
|
|
223
|
+
arising out of these terms or the use or nature of the software, under any
|
|
224
|
+
kind of legal claim.***
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
## Definitions
|
|
228
|
+
|
|
229
|
+
The **licensor** is pineforge, LLC, and the **software** is the software the
|
|
230
|
+
licensor makes available under these terms.
|
|
231
|
+
|
|
232
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
233
|
+
|
|
234
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of
|
|
235
|
+
organization that you work for, plus all organizations that have control
|
|
236
|
+
over, are under the control of, or are under common control with that
|
|
237
|
+
organization. **Control** means ownership of substantially all the assets of
|
|
238
|
+
an entity, or the power to direct its management and policies by vote,
|
|
239
|
+
contract, or otherwise. Control can be direct or indirect.
|
|
240
|
+
|
|
241
|
+
**Your licenses** are all the licenses granted to you for the software under
|
|
242
|
+
these terms.
|
|
243
|
+
|
|
244
|
+
**Use** means anything you do with the software requiring one of your
|
|
245
|
+
licenses.
|
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@ Stable releases are on the `latest` dist-tag. A prerelease (for example
|
|
|
16
16
|
- `transpile.worker.mjs` — an ES module worker that runs the glue in Pyodide.
|
|
17
17
|
- `index.mjs` — exports `release`, `tables`, `archivePath`, `sourceRoot`, `codegenSourceDir`,
|
|
18
18
|
`workerPath` and `glue`.
|
|
19
|
+
- `pineforge_codegen/diagnostics_catalog.json` — the diagnostics catalog (since 1.2.0), also
|
|
20
|
+
importable as `@pineforge/codegen-pyodide/diagnostics_catalog.json`; the repository's
|
|
21
|
+
`docs/PUBLIC_CONTRACT.md` describes it.
|
|
22
|
+
- `LICENSE` — the PineForge Source License 1.1 (since 1.2.0), which `package.json` names.
|
|
19
23
|
|
|
20
24
|
## Publishing (maintainers)
|
|
21
25
|
A `v*` tag push, which the release workflow makes, publishes through npm OIDC Trusted
|
package/glue.py
CHANGED
|
@@ -23,6 +23,10 @@ def _diagnostic_entries(diagnostics) -> list:
|
|
|
23
23
|
"col": loc.col if loc else 1,
|
|
24
24
|
"message": message,
|
|
25
25
|
"severity": getattr(d.level, "value", "error"),
|
|
26
|
+
# The stable code and named arguments of the catalog templates the
|
|
27
|
+
# message (and the hint after " — ") render from.
|
|
28
|
+
"code": d.code,
|
|
29
|
+
"args": d.args,
|
|
26
30
|
}
|
|
27
31
|
end_col = getattr(loc, "end_col", None) if loc else None
|
|
28
32
|
if end_col is not None:
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.mjs",
|
|
9
9
|
"./tables.json": "./tables.json",
|
|
10
|
-
"./release.json": "./release.json"
|
|
10
|
+
"./release.json": "./release.json",
|
|
11
|
+
"./diagnostics_catalog.json": "./pineforge_codegen/diagnostics_catalog.json"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"index.mjs",
|
|
@@ -16,7 +17,8 @@
|
|
|
16
17
|
"tables.json",
|
|
17
18
|
"release.json",
|
|
18
19
|
"transpile.worker.mjs",
|
|
19
|
-
"glue.py"
|
|
20
|
+
"glue.py",
|
|
21
|
+
"LICENSE"
|
|
20
22
|
],
|
|
21
23
|
"engines": {
|
|
22
24
|
"node": ">=20"
|
|
@@ -35,5 +37,5 @@
|
|
|
35
37
|
"type": "git",
|
|
36
38
|
"url": "git+https://github.com/pineforge-4pass/pineforge-codegen-oss.git"
|
|
37
39
|
},
|
|
38
|
-
"license": "
|
|
40
|
+
"license": "SEE LICENSE IN LICENSE"
|
|
39
41
|
}
|
|
@@ -6,6 +6,7 @@ from .lexer import Lexer
|
|
|
6
6
|
from .parser import Parser
|
|
7
7
|
from .analyzer import Analyzer
|
|
8
8
|
from .codegen import CodeGen
|
|
9
|
+
from .diagnostic_codes import diagnostics_catalog, render_diagnostic
|
|
9
10
|
from .errors import CompileError, Level, Phase
|
|
10
11
|
from .external_requests import lower_no_data_requests
|
|
11
12
|
from .builtin_keywords import bind_builtin_keywords
|
|
@@ -4414,7 +4414,44 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4414
4414
|
return None
|
|
4415
4415
|
|
|
4416
4416
|
def generate(self) -> str:
|
|
4417
|
-
"""Generate C++ source from the AnalyzerContext.
|
|
4417
|
+
"""Generate C++ source from the AnalyzerContext.
|
|
4418
|
+
|
|
4419
|
+
An error the settings metadata finds in an input's arguments (emitted
|
|
4420
|
+
in the constructor, ahead of the body: ``checked_settings``) is held
|
|
4421
|
+
until the rest is generated; the error raised is the first in source
|
|
4422
|
+
order, as before that metadata visited the arguments."""
|
|
4423
|
+
self._deferred_settings_errors: list[CompileError] | None = []
|
|
4424
|
+
try:
|
|
4425
|
+
try:
|
|
4426
|
+
cpp = self._generate_cpp()
|
|
4427
|
+
except CompileError as error:
|
|
4428
|
+
first = self._first_settings_error_before(error)
|
|
4429
|
+
if first is None:
|
|
4430
|
+
raise
|
|
4431
|
+
raise first from None
|
|
4432
|
+
if self._deferred_settings_errors:
|
|
4433
|
+
raise self._deferred_settings_errors[0]
|
|
4434
|
+
return cpp
|
|
4435
|
+
finally:
|
|
4436
|
+
self._deferred_settings_errors = None
|
|
4437
|
+
|
|
4438
|
+
def _defer_settings_error(self, error: CompileError) -> None:
|
|
4439
|
+
held = getattr(self, "_deferred_settings_errors", None)
|
|
4440
|
+
if held is None: # outside generate(): nothing holds it
|
|
4441
|
+
raise error
|
|
4442
|
+
held.append(error)
|
|
4443
|
+
|
|
4444
|
+
def _first_settings_error_before(self, error: CompileError) -> CompileError | None:
|
|
4445
|
+
"""A held settings error located before ``error``, else None."""
|
|
4446
|
+
def where(err: CompileError):
|
|
4447
|
+
loc = err.diagnostics[0].location if err.diagnostics else None
|
|
4448
|
+
return (loc.line, loc.col) if loc is not None else (float("inf"), 0)
|
|
4449
|
+
held = sorted(self._deferred_settings_errors, key=where)
|
|
4450
|
+
if held and where(held[0]) < where(error):
|
|
4451
|
+
return held[0]
|
|
4452
|
+
return None
|
|
4453
|
+
|
|
4454
|
+
def _generate_cpp(self) -> str:
|
|
4418
4455
|
# Every input is keyed by its title: refuse a non-constant one first,
|
|
4419
4456
|
# then flag inputs one override key would reach together.
|
|
4420
4457
|
self._check_input_titles()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Immutable execution metadata; it does not alter generated computation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from ..ast_nodes import BoolLiteral, FuncCall, Identifier, MemberAccess, NumberLiteral, StrategyDecl, StringLiteral, UnaryOp, VarDecl
|
|
7
|
+
from ..external_requests import CAPABILITY_UNPINNED_ANNOTATION
|
|
8
|
+
from ..limits import iter_ast_nodes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
STRATEGY_PARAMETERS = (
|
|
12
|
+
"title", "shorttitle", "overlay", "format", "precision", "scale", "pyramiding",
|
|
13
|
+
"calc_on_order_fills", "calc_on_every_tick", "max_bars_back",
|
|
14
|
+
"backtest_fill_limits_assumption", "default_qty_type", "default_qty_value",
|
|
15
|
+
"initial_capital", "currency", "slippage", "commission_type", "commission_value",
|
|
16
|
+
"process_orders_on_close", "close_entries_rule", "margin_long", "margin_short",
|
|
17
|
+
"explicit_plot_zorder", "max_lines_count", "max_labels_count", "max_boxes_count",
|
|
18
|
+
"calc_bars_count", "risk_free_rate", "use_bar_magnifier", "fill_orders_on_standard_ohlc",
|
|
19
|
+
"max_polylines_count", "dynamic_requests", "behind_chart", "calc_on_every_history_tick",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DECLARATION_DEFAULTS = {
|
|
24
|
+
"calc_on_every_tick": False,
|
|
25
|
+
"calc_on_order_fills": False,
|
|
26
|
+
"process_orders_on_close": False,
|
|
27
|
+
"use_bar_magnifier": False,
|
|
28
|
+
"fill_orders_on_standard_ohlc": False,
|
|
29
|
+
"backtest_fill_limits_assumption": 0,
|
|
30
|
+
"currency": "currency.NONE",
|
|
31
|
+
"timeframe": "",
|
|
32
|
+
"timeframe_gaps": True,
|
|
33
|
+
"dynamic_requests": True,
|
|
34
|
+
"calc_on_every_history_tick": False,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _expression(node):
|
|
39
|
+
if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
|
|
40
|
+
return node.value
|
|
41
|
+
if isinstance(node, Identifier):
|
|
42
|
+
return node.name
|
|
43
|
+
if isinstance(node, MemberAccess):
|
|
44
|
+
return f"{_expression(node.object)}.{node.member}"
|
|
45
|
+
if isinstance(node, FuncCall):
|
|
46
|
+
return f"{_expression(node.callee)}({','.join(str(_expression(arg)) for arg in node.args)})"
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _literal(node):
|
|
51
|
+
if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
|
|
52
|
+
return node.value, True
|
|
53
|
+
if isinstance(node, UnaryOp) and node.op in ("+", "-"):
|
|
54
|
+
value, valid = _literal(node.operand)
|
|
55
|
+
if valid and type(value) in (int, float):
|
|
56
|
+
return value if node.op == "+" else -value, True
|
|
57
|
+
if isinstance(node, MemberAccess):
|
|
58
|
+
value = _expression(node)
|
|
59
|
+
if value and value.split(".")[0] in ("currency", "format", "scale", "strategy"):
|
|
60
|
+
return value, True
|
|
61
|
+
return _expression(node), False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def capabilities_document(emitter) -> str:
|
|
65
|
+
declarations = dict(DECLARATION_DEFAULTS)
|
|
66
|
+
unresolved = []
|
|
67
|
+
requests = []
|
|
68
|
+
lower_symbols = {}
|
|
69
|
+
intrabar = False
|
|
70
|
+
recorded = False
|
|
71
|
+
for node, _depth in iter_ast_nodes(emitter.ctx.ast):
|
|
72
|
+
if isinstance(node, StrategyDecl):
|
|
73
|
+
arguments = {STRATEGY_PARAMETERS[index] if index < len(STRATEGY_PARAMETERS)
|
|
74
|
+
else f"strategy() positional argument {index + 1}": argument
|
|
75
|
+
for index, argument in enumerate(node.args)}
|
|
76
|
+
unresolved.extend(arguments.keys() & node.kwargs.keys())
|
|
77
|
+
arguments.update(node.kwargs)
|
|
78
|
+
for name, argument in arguments.items():
|
|
79
|
+
value, valid = _literal(argument)
|
|
80
|
+
default = DECLARATION_DEFAULTS.get(name)
|
|
81
|
+
if name in DECLARATION_DEFAULTS:
|
|
82
|
+
valid &= type(value) is type(default)
|
|
83
|
+
if name == "backtest_fill_limits_assumption":
|
|
84
|
+
valid = (type(value) is int and value >= 0) or (
|
|
85
|
+
type(value) is float and math.isfinite(value) and value >= 0 and value == int(value))
|
|
86
|
+
if valid:
|
|
87
|
+
value = int(value)
|
|
88
|
+
declarations[name] = value
|
|
89
|
+
if not valid or name not in STRATEGY_PARAMETERS and name not in DECLARATION_DEFAULTS:
|
|
90
|
+
unresolved.append(name)
|
|
91
|
+
elif isinstance(node, VarDecl):
|
|
92
|
+
intrabar |= node.is_varip
|
|
93
|
+
elif isinstance(node, FuncCall):
|
|
94
|
+
name, namespace = emitter._resolve_callee(node.callee)
|
|
95
|
+
if namespace == "request" and name == "security_lower_tf":
|
|
96
|
+
expression = node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
|
|
97
|
+
lower_symbols[id(expression)] = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
98
|
+
parts = (node.annotations or {}).get("pf_recorded_key")
|
|
99
|
+
if parts is not None:
|
|
100
|
+
recorded = True
|
|
101
|
+
requests.append({
|
|
102
|
+
"function": f"{namespace}.{name}",
|
|
103
|
+
"symbol": _expression(node.args[0]) if node.args else None,
|
|
104
|
+
"timeframe": "",
|
|
105
|
+
"lookahead": f"barmerge.lookahead_{parts.get('lookahead', 'off')}",
|
|
106
|
+
"gaps": f"barmerge.gaps_{parts.get('gaps', 'off')}",
|
|
107
|
+
"heikinashi": False,
|
|
108
|
+
"feed": "recorded",
|
|
109
|
+
})
|
|
110
|
+
if isinstance(node, Identifier) and node.name == "timenow":
|
|
111
|
+
unresolved.append("timenow (bar timestamp approximation)")
|
|
112
|
+
if isinstance(node, Identifier) and node.name in ("last_bar_index", "last_bar_time"):
|
|
113
|
+
unresolved.append(f"{node.name} (stream endpoint is not batch-equivalent)")
|
|
114
|
+
if isinstance(node, MemberAccess) and _expression(node) == "barstate.isrealtime":
|
|
115
|
+
unresolved.append("barstate.isrealtime (compiled historical-only approximation)")
|
|
116
|
+
if isinstance(node, MemberAccess) and _expression(node) in (
|
|
117
|
+
"barstate.islast", "barstate.islastconfirmedhistory"):
|
|
118
|
+
unresolved.append(f"{_expression(node)} (stream endpoint is not batch-equivalent)")
|
|
119
|
+
|
|
120
|
+
auxiliary = False
|
|
121
|
+
for site in emitter._security_calls:
|
|
122
|
+
symbol_node = site.get("symbol_node")
|
|
123
|
+
if site.get("is_lower_tf_array"):
|
|
124
|
+
symbol_node = lower_symbols.get(id(site.get("expr_node")), symbol_node)
|
|
125
|
+
if isinstance(symbol_node, FuncCall) and site.get("heikinashi") and symbol_node.args:
|
|
126
|
+
symbol_node = symbol_node.args[0]
|
|
127
|
+
symbol = _expression(symbol_node) if symbol_node is not None else "syminfo.tickerid"
|
|
128
|
+
timeframe = _expression(site.get("tf_node"))
|
|
129
|
+
lookahead = _expression(site.get("lookahead_node")) or "barmerge.lookahead_off"
|
|
130
|
+
gaps = _expression(site.get("gaps_node")) or "barmerge.gaps_off"
|
|
131
|
+
if isinstance(symbol_node, Identifier) and symbol_node.name not in emitter._global_mutable_infos:
|
|
132
|
+
symbol = _expression((getattr(emitter.ctx, "global_expr_map", {}) or {}).get(
|
|
133
|
+
symbol_node.name, symbol_node))
|
|
134
|
+
feed = "chart" if symbol in ("syminfo.tickerid", "syminfo.ticker", "") else "auxiliary"
|
|
135
|
+
auxiliary |= feed == "auxiliary"
|
|
136
|
+
if not isinstance(site.get("tf_node"), StringLiteral) and timeframe != "timeframe.period":
|
|
137
|
+
unresolved.append(f"request.security[{site['sec_id']}].timeframe")
|
|
138
|
+
if lookahead not in ("barmerge.lookahead_off", "barmerge.lookahead_on"):
|
|
139
|
+
unresolved.append(f"request.security[{site['sec_id']}].lookahead")
|
|
140
|
+
requests.append({
|
|
141
|
+
"function": "request.security_lower_tf" if site.get("is_lower_tf_array") else "request.security",
|
|
142
|
+
"symbol": symbol,
|
|
143
|
+
"timeframe": timeframe,
|
|
144
|
+
"lookahead": lookahead,
|
|
145
|
+
"gaps": gaps,
|
|
146
|
+
"heikinashi": bool(site.get("heikinashi")),
|
|
147
|
+
"feed": feed,
|
|
148
|
+
})
|
|
149
|
+
for index, site in enumerate((emitter.ctx.ast.annotations or {}).get(CAPABILITY_UNPINNED_ANNOTATION, ())):
|
|
150
|
+
requests.append({
|
|
151
|
+
"function": site["function"],
|
|
152
|
+
"symbol": _expression(site["symbol_node"]),
|
|
153
|
+
"timeframe": _expression(site["tf_node"]) if site["function"] in (
|
|
154
|
+
"request.security", "request.security_lower_tf") else "",
|
|
155
|
+
"lookahead": _expression(site["lookahead_node"]) or "barmerge.lookahead_off",
|
|
156
|
+
"gaps": _expression(site["gaps_node"]) or "barmerge.gaps_off",
|
|
157
|
+
"heikinashi": False,
|
|
158
|
+
"feed": "unpinned",
|
|
159
|
+
})
|
|
160
|
+
unresolved.append(f"{site['function']}[unpinned:{index}]")
|
|
161
|
+
requirements = {
|
|
162
|
+
"auxiliary_security_feeds": auxiliary,
|
|
163
|
+
"native_security_feeds": False,
|
|
164
|
+
"fx_curve": declarations["currency"] != "currency.NONE",
|
|
165
|
+
"recorded_series": recorded,
|
|
166
|
+
"historical_probe_overrides": False,
|
|
167
|
+
"intrabar_persistence": intrabar,
|
|
168
|
+
}
|
|
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)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def emit_capabilities_exports(emitter, lines: list[str]) -> None:
|
|
175
|
+
literal = json.dumps(capabilities_document(emitter))
|
|
176
|
+
lines.extend([
|
|
177
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
178
|
+
"#ifdef PF_CAPABILITIES_API_VERSION",
|
|
179
|
+
" uint32_t strategy_capabilities_api_version(void) { return PF_CAPABILITIES_API_VERSION; }",
|
|
180
|
+
" int strategy_capabilities_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
|
|
181
|
+
" if (required) *required = 0;",
|
|
182
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
183
|
+
' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
|
|
184
|
+
f" ::pineforge::checked_settings::receipt({literal}, json, capacity, required);",
|
|
185
|
+
" });", " }", "#endif", "#endif",
|
|
186
|
+
])
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
"""Opt-in settings metadata, validation and exception-contained C exports."""
|
|
2
2
|
|
|
3
3
|
from ..ast_nodes import Identifier, MemberAccess
|
|
4
|
+
from ..errors import CompileError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _visit_setting_arg(emitter, expr) -> str:
|
|
8
|
+
"""An input argument's C++ for the settings metadata. The metadata is
|
|
9
|
+
emitted in the constructor, ahead of the script body: an error here is
|
|
10
|
+
held until the body is generated (``CodeGen.generate``), so the script's
|
|
11
|
+
first error in source order is the one raised."""
|
|
12
|
+
try:
|
|
13
|
+
return emitter._visit_expr(expr)
|
|
14
|
+
except CompileError as error:
|
|
15
|
+
emitter._defer_settings_error(error)
|
|
16
|
+
return "0"
|
|
4
17
|
|
|
5
18
|
|
|
6
19
|
def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
|
|
@@ -38,7 +51,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
38
51
|
key = emitter._input_key_literal(name)
|
|
39
52
|
default = emitter._get_input_default(node)
|
|
40
53
|
getter = emitter._input_getter_for_call(node, func_name, namespace)
|
|
41
|
-
default_cpp = emitter
|
|
54
|
+
default_cpp = _visit_setting_arg(emitter, default) if default is not None else "0"
|
|
42
55
|
default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
|
|
43
56
|
value_type = {
|
|
44
57
|
"get_input_int": "int", "get_input_int64": "int",
|
|
@@ -89,7 +102,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
89
102
|
declared = getattr(arguments.get("options"), "elements", None)
|
|
90
103
|
if declared:
|
|
91
104
|
for option in declared:
|
|
92
|
-
option_cpp = emitter
|
|
105
|
+
option_cpp = _visit_setting_arg(emitter, option)
|
|
93
106
|
options.append(option_cpp if getter == "get_input_string" else
|
|
94
107
|
f'::pineforge::checked_settings::number({option_cpp})')
|
|
95
108
|
expression = f'{getter}({key}, {default_cpp})'
|
|
@@ -98,7 +111,7 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
98
111
|
if default_serialized is None:
|
|
99
112
|
default_serialized = (default_cpp if value_type in ("string", "source") else
|
|
100
113
|
f'::pineforge::checked_settings::number({default_cpp})')
|
|
101
|
-
constraints = [emitter
|
|
114
|
+
constraints = [_visit_setting_arg(emitter, arguments[name]) if arguments.get(name) is not None
|
|
102
115
|
else "std::numeric_limits<double>::quiet_NaN()"
|
|
103
116
|
for name in ("minval", "maxval", "step")]
|
|
104
117
|
metadata = (f'{{{key}, "{value_type}", {default_serialized}, '
|
|
@@ -1998,6 +1998,8 @@ class TopLevelEmitter:
|
|
|
1998
1998
|
lines.append(" }")
|
|
1999
1999
|
from .checked_settings import emit_settings_exports
|
|
2000
2000
|
emit_settings_exports(lines)
|
|
2001
|
+
from .capabilities import emit_capabilities_exports
|
|
2002
|
+
emit_capabilities_exports(self, lines)
|
|
2001
2003
|
if self._declares_bar_magnifier():
|
|
2002
2004
|
# TradingView runs a script that declares use_bar_magnifier = true
|
|
2003
2005
|
# on its bar magnifier; the host reads this export to run it on
|
|
@@ -282,6 +282,7 @@ CPP_EMITTER_NAMES = frozenset("""
|
|
|
282
282
|
configure_security_evaluators snapshot_script_state restore_script_state
|
|
283
283
|
commit_script_state set_strategy_override set_input
|
|
284
284
|
set_magnifier_volume_weighted strategy_declares_bar_magnifier fill_report run
|
|
285
|
+
strategy_capabilities_api_version strategy_capabilities_receipt
|
|
285
286
|
precalculate
|
|
286
287
|
strategy_entry strategy_close strategy_close_all strategy_exit
|
|
287
288
|
strategy_exit_cancel_bracket strategy_cancel strategy_cancel_all strategy_order
|