pineforge-codegen 0.6.5__tar.gz → 0.7.0__tar.gz
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.
- pineforge_codegen-0.7.0/LEGAL.md +32 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/PKG-INFO +1 -1
- pineforge_codegen-0.7.0/VERSION +1 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/base.py +5 -2
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/types.py +11 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/helpers_syminfo.py +9 -11
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/tables.py +81 -11
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/types.py +19 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/visit_call.py +160 -78
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/visit_expr.py +31 -3
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/visit_stmt.py +45 -8
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/support_checker.py +123 -11
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/golden/matrix_eigen_pca.cpp +2 -9
- pineforge_codegen-0.7.0/tests/test_codegen_audit_fixes.py +288 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_signatures.py +2 -1
- pineforge_codegen-0.7.0/tests/test_support_checker_const_namespaces.py +141 -0
- pineforge_codegen-0.7.0/tests/test_support_checker_syminfo_gap_fields.py +71 -0
- pineforge_codegen-0.6.5/VERSION +0 -1
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/.github/dependabot.yml +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/.github/workflows/release.yml +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/.gitignore +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/CLAUDE.md +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/LICENSE +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/README.md +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/docs/codegen-coverage-gaps.md +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/__init__.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/__init__.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/call_handlers.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/contracts.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/diagnostics.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/analyzer/tables.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/ast_nodes.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/__init__.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/base.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/emit_top.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/helpers.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/input.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/security.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/ta.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/errors.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/lexer.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/parser.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/pragmas.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/signatures.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/symbols.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/tokens.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/tv_input_choices.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pyproject.toml +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/__init__.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/_compile.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_analyzer.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_analyzer_matrix_inference.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_analyzer_ta_return_types.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_codegen_fallthrough_guards.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_codegen_golden.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_codegen_input_getters.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_codegen_matrix_typed.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_codegen_new.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_compile_corpus.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_compile_smoke.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_errors.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_input_time_int64.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_int64_time_storage.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_lexer.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_official_surface.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_parser.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_security_tf_literal.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_chart_visible_bar_time.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_color_cast.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_dividends_earnings.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_footprint.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_input_color.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_input_source.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_matrix.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_security_adjustment.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_timeframe_from_seconds.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_varip.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_support_checker_volume_row.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_symbols.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_ta_official_surface.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_transpile_division.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_transpile_enum_order.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_transpile_pf_trace.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_transpile_tr_handle_na.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_transpiler_matrix_kwargs.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_typespec_matrix.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_udt_drawing_field_cleanup.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_unsupported_reporting.py +0 -0
- {pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/tests/test_vwap_tuple_unpack.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Legal information
|
|
2
|
+
|
|
3
|
+
Summary of licensing, third-party components, and trademarks for `pineforge-codegen`. **Not** legal advice; consult counsel for your use case.
|
|
4
|
+
|
|
5
|
+
## License
|
|
6
|
+
|
|
7
|
+
`pineforge-codegen` is **source-available**, **not** OSI "open source." It is distributed under the **PolyForm Noncommercial License 1.0.0** with two supplemental terms — see [LICENSE](LICENSE), which is the controlling text:
|
|
8
|
+
|
|
9
|
+
- **Personal Trading exception** — free to research, backtest, and trade your **own** capital, for an individual acting on their own behalf.
|
|
10
|
+
- **Commercial use** — companies, funds, managing third-party capital, embedding in a product, or operating a hosted/public-facing service requires a **commercial license** (email **luis@4pass.com.tw**).
|
|
11
|
+
|
|
12
|
+
Describe this project as **"source-available"** rather than "open source." The runtime it targets, [`pineforge-engine`](https://github.com/pineforge-4pass/pineforge-engine), is separate and **Apache-2.0**.
|
|
13
|
+
|
|
14
|
+
## What this repository is
|
|
15
|
+
|
|
16
|
+
A pure-Python PineScript v6 → C++ transpiler that emits source against the public PineForge engine C-ABI. It implements PineScript v6 from **TradingView's publicly published language documentation**; it does **not** incorporate TradingView proprietary source. Names like "PineScript v6" are used **nominatively** to describe the input language.
|
|
17
|
+
|
|
18
|
+
## Third-party components
|
|
19
|
+
|
|
20
|
+
The transpiler has **no runtime dependencies** (one function, `transpile()`). Development/test extras declared in `pyproject.toml` (`pytest`, etc.) are under their own upstream licenses. The opt-in compile checks invoke a C++ compiler against the separately-licensed Apache-2.0 engine headers; they skip cleanly without an engine checkout.
|
|
21
|
+
|
|
22
|
+
## Trademarks and affiliation
|
|
23
|
+
|
|
24
|
+
**TradingView** and **PineScript** are trademarks of their respective owners. `pineforge-codegen` is **not** affiliated with, endorsed by, or certified by TradingView. References to "PineScript v6" and any compatibility/parity statements are **nominative** and factual — compatibility and technical testing only, not a partnership or certification.
|
|
25
|
+
|
|
26
|
+
## Contributions
|
|
27
|
+
|
|
28
|
+
Contributions are accepted under a Developer Certificate of Origin (`Signed-off-by`). Because this project is **dual-licensed** (source-available + a sold commercial license), material contributions require a Contributor License Agreement granting PineForge the right to include the contribution in the commercial license; otherwise it cannot be accepted. See `CONTRIBUTING` (or contact luis@4pass.com.tw) before opening a material PR.
|
|
29
|
+
|
|
30
|
+
## No warranty
|
|
31
|
+
|
|
32
|
+
Provided **"AS IS"**, without warranty of any kind, per the LICENSE's no-warranty / no-liability terms. Transpiler output and any downstream backtest results are **not** investment advice and carry no warranty of trading outcomes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pineforge-codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: PineScript v6 to C++ transpiler that targets the pineforge-engine runtime.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pineforge-4pass/pineforge-codegen-oss
|
|
6
6
|
Project-URL: Issues, https://github.com/pineforge-4pass/pineforge-codegen-oss/issues
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.7.0
|
|
@@ -1426,9 +1426,12 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
1426
1426
|
if ns == "order":
|
|
1427
1427
|
return PineType.INT
|
|
1428
1428
|
|
|
1429
|
-
# format.* constants
|
|
1429
|
+
# format.* constants — Pine ``const string`` ("mintick",
|
|
1430
|
+
# "percent", ...). Codegen emits them as std::string literals
|
|
1431
|
+
# (consumed by pine_str_tostring), so bare reads must type STRING
|
|
1432
|
+
# for the declared C++ variable to match.
|
|
1430
1433
|
if ns == "format":
|
|
1431
|
-
return PineType.
|
|
1434
|
+
return PineType.STRING
|
|
1432
1435
|
|
|
1433
1436
|
# text.* constants (align_left, align_right, etc.)
|
|
1434
1437
|
if ns == "text":
|
|
@@ -132,6 +132,17 @@ class TypeHelper:
|
|
|
132
132
|
first = self._visit(value.args[0])
|
|
133
133
|
return TypeSpec.array(self._pine_type_to_spec(first))
|
|
134
134
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
135
|
+
# Functional-form array element/copy accessors: the receiver is
|
|
136
|
+
# the first argument (``array.copy(arr)``), mirroring the
|
|
137
|
+
# method-form handling below (``arr.copy()``).
|
|
138
|
+
if (ns == "array" and value.args
|
|
139
|
+
and func in ("copy", "slice", "get", "first", "last",
|
|
140
|
+
"pop", "shift", "remove")):
|
|
141
|
+
arg_spec = self._type_spec_from_expr(value.args[0])
|
|
142
|
+
if arg_spec is not None and arg_spec.kind == "array":
|
|
143
|
+
if func in ("copy", "slice"):
|
|
144
|
+
return arg_spec
|
|
145
|
+
return arg_spec.element
|
|
135
146
|
if ns == "matrix" and func == "new":
|
|
136
147
|
if targs:
|
|
137
148
|
elem = self._type_spec_from_hint(targs[0]) or TypeSpec.udt(targs[0])
|
{pineforge_codegen-0.6.5 → pineforge_codegen-0.7.0}/pineforge_codegen/codegen/helpers_syminfo.py
RENAMED
|
@@ -7,7 +7,7 @@ engine changes:
|
|
|
7
7
|
- ``_pf_derive_main_tickerid(tickerid)`` — strip futures suffix from tickerid
|
|
8
8
|
e.g., ``"CME_MINI:ES1!"`` → ``"CME_MINI:ES"``, ``"NASDAQ:AAPL"`` → ``"NASDAQ:AAPL"``
|
|
9
9
|
- ``_pf_derive_country(tickerid)`` — lookup country by exchange prefix
|
|
10
|
-
e.g., ``"NASDAQ:AAPL"`` → ``"US"``, ``"LSE:BP"`` → ``"
|
|
10
|
+
e.g., ``"NASDAQ:AAPL"`` → ``"US"``, ``"LSE:BP"`` → ``"GB"`` (ISO 3166-1)
|
|
11
11
|
|
|
12
12
|
These are emitted as ``static inline`` free functions before the
|
|
13
13
|
``GeneratedStrategy`` class definition. They depend only on ``<string>``
|
|
@@ -15,7 +15,12 @@ and ``<regex>`` (both already pulled in by the standard includes block).
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
# Prefix → country lookup table used for ``syminfo.country`` derivation.
|
|
18
|
-
# Mirrors Pine v6 semantics best-effort; not an exhaustive list.
|
|
18
|
+
# Mirrors Pine v6 semantics best-effort; not an exhaustive list. All values
|
|
19
|
+
# MUST be ISO 3166-1 alpha-2 codes (Pine returns ISO codes: LSE → "GB", not
|
|
20
|
+
# "UK"). Prefixes with no single ISO country (pan-European EURONEXT, global
|
|
21
|
+
# crypto venues BINANCE/KRAKEN/BYBIT/OKX/BITMEX/DERIBIT) are intentionally
|
|
22
|
+
# absent — the helper returns na<std::string>() for them, matching TV's na
|
|
23
|
+
# for symbols without a listing country.
|
|
19
24
|
PREFIX_TO_COUNTRY: dict[str, str] = {
|
|
20
25
|
"NASDAQ": "US",
|
|
21
26
|
"NYSE": "US",
|
|
@@ -28,24 +33,17 @@ PREFIX_TO_COUNTRY: dict[str, str] = {
|
|
|
28
33
|
"CBOT": "US",
|
|
29
34
|
"COMEX": "US",
|
|
30
35
|
"OTC": "US",
|
|
31
|
-
"LSE": "
|
|
32
|
-
"AQUIS": "
|
|
36
|
+
"LSE": "GB",
|
|
37
|
+
"AQUIS": "GB",
|
|
33
38
|
"TSE": "JP",
|
|
34
39
|
"OSE": "JP",
|
|
35
40
|
"HKEX": "HK",
|
|
36
41
|
"SGX": "SG",
|
|
37
42
|
"ASX": "AU",
|
|
38
|
-
"EURONEXT": "EU",
|
|
39
43
|
"XETRA": "DE",
|
|
40
44
|
"BSE": "IN",
|
|
41
45
|
"NSE": "IN",
|
|
42
|
-
"BINANCE": "GLOBAL",
|
|
43
46
|
"COINBASE": "US",
|
|
44
|
-
"KRAKEN": "GLOBAL",
|
|
45
|
-
"BYBIT": "GLOBAL",
|
|
46
|
-
"OKX": "GLOBAL",
|
|
47
|
-
"BITMEX": "GLOBAL",
|
|
48
|
-
"DERIBIT": "GLOBAL",
|
|
49
47
|
"UPBIT": "KR",
|
|
50
48
|
"KRX": "KR",
|
|
51
49
|
"KOSPI": "KR",
|
|
@@ -28,6 +28,53 @@ BAR_FIELDS = {
|
|
|
28
28
|
"low": "current_bar_.low", "volume": "current_bar_.volume",
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
# struct-tm field extraction expressions for the time/date builtins. Shared
|
|
32
|
+
# between the bare variable forms (``hour`` -> BAR_BUILTINS below) and the
|
|
33
|
+
# function forms (``hour(time[, tz])`` in visit_call.py) so the two cannot
|
|
34
|
+
# drift apart numerically.
|
|
35
|
+
TIME_FIELD_EXPRS = {
|
|
36
|
+
"year": "tm_buf.tm_year + 1900",
|
|
37
|
+
"month": "tm_buf.tm_mon + 1",
|
|
38
|
+
"dayofmonth": "tm_buf.tm_mday",
|
|
39
|
+
"dayofweek": "tm_buf.tm_wday + 1",
|
|
40
|
+
"hour": "tm_buf.tm_hour",
|
|
41
|
+
"minute": "tm_buf.tm_min",
|
|
42
|
+
"second": "tm_buf.tm_sec",
|
|
43
|
+
"weekofyear": "(tm_buf.tm_yday + 7 - ((tm_buf.tm_wday + 6) % 7)) / 7",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
|
|
48
|
+
"""Inline C++ lambda decomposing a Unix-ms timestamp in a timezone.
|
|
49
|
+
|
|
50
|
+
UTC / "" / "Etc/UTC" stays on the cheap ``gmtime_r`` fast path (matching
|
|
51
|
+
the engine's ``_decompose_bar_time``); anything else takes a
|
|
52
|
+
mutex-guarded setenv+``localtime_r`` block, mirroring
|
|
53
|
+
``pine_tz::ScopedTimezone`` (src/timezone.cpp) which is not exposed via
|
|
54
|
+
any public ``<pineforge/...>`` header today.
|
|
55
|
+
"""
|
|
56
|
+
return (
|
|
57
|
+
"[&]() -> int { "
|
|
58
|
+
f"std::string _tz = ({tz_arg}); "
|
|
59
|
+
f"time_t _secs = (time_t)(({ts_arg}) / 1000); "
|
|
60
|
+
"struct tm tm_buf; "
|
|
61
|
+
"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") { "
|
|
62
|
+
"gmtime_r(&_secs, &tm_buf); "
|
|
63
|
+
"} else { "
|
|
64
|
+
"static std::mutex _pf_tz_mu; "
|
|
65
|
+
"std::lock_guard<std::mutex> _pf_tz_lock(_pf_tz_mu); "
|
|
66
|
+
"const char* _old = std::getenv(\"TZ\"); "
|
|
67
|
+
"std::string _old_tz = _old ? _old : \"\"; bool _had_old = (_old != nullptr); "
|
|
68
|
+
"::setenv(\"TZ\", _tz.c_str(), 1); ::tzset(); "
|
|
69
|
+
"localtime_r(&_secs, &tm_buf); "
|
|
70
|
+
"if (_had_old) { ::setenv(\"TZ\", _old_tz.c_str(), 1); } "
|
|
71
|
+
"else { ::unsetenv(\"TZ\"); } ::tzset(); "
|
|
72
|
+
"} "
|
|
73
|
+
f"return {field_expr}; "
|
|
74
|
+
"}()"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
31
78
|
BAR_BUILTINS = {
|
|
32
79
|
"bar_index": "bar_index_",
|
|
33
80
|
"time": "current_bar_.timestamp",
|
|
@@ -42,15 +89,20 @@ BAR_BUILTINS = {
|
|
|
42
89
|
"hlc3": "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)",
|
|
43
90
|
"hlcc4": "((current_bar_.high + current_bar_.low + current_bar_.close + current_bar_.close) / 4.0)",
|
|
44
91
|
"ohlc4": "((current_bar_.open + current_bar_.high + current_bar_.low + current_bar_.close) / 4.0)",
|
|
45
|
-
# Time/date extraction from bar timestamp
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
# Time/date extraction from the bar timestamp. Pine v6 specifies the
|
|
93
|
+
# EXCHANGE timezone (syminfo.timezone) for the bare variable forms, so
|
|
94
|
+
# they route through the same timezone-aware lambda as the function
|
|
95
|
+
# forms ``hour(time[, tz])`` instead of the engine's UTC-only
|
|
96
|
+
# ``_bar_hour()`` helpers. For UTC-exchange data (the crypto corpus,
|
|
97
|
+
# SymInfo's constructor default) the lambda takes the gmtime_r fast
|
|
98
|
+
# path and is value-identical to the old emission.
|
|
99
|
+
**{
|
|
100
|
+
name: tz_time_field_lambda(
|
|
101
|
+
TIME_FIELD_EXPRS[name], "current_bar_.timestamp", "syminfo_.timezone"
|
|
102
|
+
)
|
|
103
|
+
for name in ("hour", "minute", "second", "dayofmonth", "dayofweek",
|
|
104
|
+
"month", "year", "weekofyear")
|
|
105
|
+
},
|
|
54
106
|
}
|
|
55
107
|
|
|
56
108
|
BAR_SERIES_PUSH = {
|
|
@@ -308,9 +360,12 @@ ADJUSTMENT_MAP = {"none": "0", "dividends": "1", "splits": "2"}
|
|
|
308
360
|
|
|
309
361
|
# display.* (plot_display) constants — ints for C++. TV uses these in chart
|
|
310
362
|
# settings; the backtest ignores them. Unknown member falls back to "all" (0).
|
|
363
|
+
# The integer codes are inert downstream (no engine consumer); they only need
|
|
364
|
+
# to be distinct so constant-equality comparisons behave.
|
|
311
365
|
DISPLAY_MAP = {
|
|
312
366
|
"all": "0", "none": "1", "pane": "2",
|
|
313
367
|
"data_window": "3", "status_line": "4", "price_scale": "5",
|
|
368
|
+
"pine_screener": "6",
|
|
314
369
|
}
|
|
315
370
|
|
|
316
371
|
# order.* sort-direction constants — emitted as std::string literals. Unknown
|
|
@@ -359,8 +414,23 @@ ARRAY_METHODS = {
|
|
|
359
414
|
"range": lambda a, args: f"(*std::max_element({a}.begin(),{a}.end())-*std::min_element({a}.begin(),{a}.end()))",
|
|
360
415
|
"every": lambda a, args: f"std::all_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
|
|
361
416
|
"some": lambda a, args: f"std::any_of({a}.begin(),{a}.end(),[](double v){{return v!=0.0;}})",
|
|
362
|
-
|
|
363
|
-
|
|
417
|
+
# stdev/variance honor the optional 2nd ``biased`` arg (Pine v6:
|
|
418
|
+
# biased=true → population (default), false → sample / n-1). The no-arg
|
|
419
|
+
# form keeps the original population emission byte-identical.
|
|
420
|
+
"stdev": lambda a, args: (
|
|
421
|
+
f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); "
|
|
422
|
+
f"double _d=({args[0]})?(double){a}.size():((double){a}.size()-1.0); "
|
|
423
|
+
f"return _d>0?std::sqrt(s/_d):na<double>(); }}()"
|
|
424
|
+
if args else
|
|
425
|
+
f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return std::sqrt(s/{a}.size()); }}()"
|
|
426
|
+
),
|
|
427
|
+
"variance": lambda a, args: (
|
|
428
|
+
f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); "
|
|
429
|
+
f"double _d=({args[0]})?(double){a}.size():((double){a}.size()-1.0); "
|
|
430
|
+
f"return _d>0?s/_d:na<double>(); }}()"
|
|
431
|
+
if args else
|
|
432
|
+
f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); return s/{a}.size(); }}()"
|
|
433
|
+
),
|
|
364
434
|
"median": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int n=c.size(); return n%2?c[n/2]:(c[n/2-1]+c[n/2])/2.0; }}()",
|
|
365
435
|
"mode": lambda a, args: f"[&](){{ std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
|
|
366
436
|
"percentile_linear_interpolation": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
|
|
@@ -209,6 +209,17 @@ class TypeInferer:
|
|
|
209
209
|
if func_name == "from" and node.args:
|
|
210
210
|
return TypeSpec.array(self._type_spec_from_expr(node.args[0]) or TypeSpec.primitive("float"))
|
|
211
211
|
return TypeSpec.array(TypeSpec.primitive("float"))
|
|
212
|
+
# Functional-form array element/copy accessors: the receiver is
|
|
213
|
+
# the first argument (``array.copy(arr)``), mirroring the
|
|
214
|
+
# method-form handling below (``arr.copy()``).
|
|
215
|
+
if (namespace == "array" and node.args
|
|
216
|
+
and func_name in ("copy", "slice", "get", "first", "last",
|
|
217
|
+
"pop", "shift", "remove")):
|
|
218
|
+
arg_spec = self._type_spec_from_expr(node.args[0])
|
|
219
|
+
if arg_spec is not None and arg_spec.kind == "array":
|
|
220
|
+
if func_name in ("copy", "slice"):
|
|
221
|
+
return arg_spec
|
|
222
|
+
return arg_spec.element
|
|
212
223
|
if namespace == "map" and func_name == "new":
|
|
213
224
|
key = self._type_spec_from_hint_name(targs[0]) if len(targs) > 0 else TypeSpec.primitive("string")
|
|
214
225
|
val = self._type_spec_from_hint_name(targs[1]) if len(targs) > 1 else TypeSpec.primitive("float")
|
|
@@ -471,6 +482,10 @@ class TypeInferer:
|
|
|
471
482
|
return "std::string"
|
|
472
483
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
473
484
|
return "std::vector<double>"
|
|
485
|
+
# array.join returns string in both the functional and the
|
|
486
|
+
# method-call forms.
|
|
487
|
+
if namespace == "array" and func_name == "join":
|
|
488
|
+
return "std::string"
|
|
474
489
|
if isinstance(node.callee, MemberAccess):
|
|
475
490
|
member_name = func_name or node.callee.member
|
|
476
491
|
recv_spec = self._type_spec_from_expr(node.callee.object)
|
|
@@ -508,6 +523,10 @@ class TypeInferer:
|
|
|
508
523
|
ename = node.object.name
|
|
509
524
|
if ename in self._enum_defs and node.member in self._enum_defs[ename]:
|
|
510
525
|
return "int"
|
|
526
|
+
# format.* constants emit std::string literals (consumed by
|
|
527
|
+
# pine_str_tostring); bare reads must declare std::string.
|
|
528
|
+
if ename == "format":
|
|
529
|
+
return "std::string"
|
|
511
530
|
# syminfo.* type inference: look up in SYMINFO_MEMBER_MAP
|
|
512
531
|
# and derive C++ type from the expression (na<T>() or function call).
|
|
513
532
|
if ename == "syminfo":
|
|
@@ -155,11 +155,46 @@ from .tables import (
|
|
|
155
155
|
SKIP_NAMESPACES,
|
|
156
156
|
SKIP_VAR_TYPES,
|
|
157
157
|
STR_FUNC_MAP,
|
|
158
|
+
TIME_FIELD_EXPRS,
|
|
158
159
|
_merge_kwargs,
|
|
159
160
|
_merge_kwargs_with_defaults,
|
|
161
|
+
tz_time_field_lambda,
|
|
160
162
|
)
|
|
161
163
|
|
|
162
164
|
|
|
165
|
+
def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
166
|
+
"""Parse a Pine ``timestamp(dateString)`` literal to Unix milliseconds.
|
|
167
|
+
|
|
168
|
+
Pine v6 accepts ISO-8601 strings ("2025-01-01", "2011-10-10T14:48:00",
|
|
169
|
+
with optional offset) and the "DD MMM YYYY hh:mm:ss ±HHMM" /
|
|
170
|
+
"MMM DD YYYY ..." forms. A dateString without a time zone is GMT+0 per
|
|
171
|
+
the Pine reference. Returns None when the string cannot be parsed.
|
|
172
|
+
"""
|
|
173
|
+
from datetime import datetime, timezone
|
|
174
|
+
|
|
175
|
+
txt = text.strip()
|
|
176
|
+
dt = None
|
|
177
|
+
try:
|
|
178
|
+
dt = datetime.fromisoformat(txt)
|
|
179
|
+
except ValueError:
|
|
180
|
+
for fmt in (
|
|
181
|
+
"%d %b %Y %H:%M:%S %z", "%d %b %Y %H:%M %z",
|
|
182
|
+
"%d %b %Y %H:%M:%S", "%d %b %Y %H:%M", "%d %b %Y",
|
|
183
|
+
"%b %d %Y %H:%M:%S %z", "%b %d %Y %H:%M %z",
|
|
184
|
+
"%b %d %Y %H:%M:%S", "%b %d %Y %H:%M", "%b %d %Y",
|
|
185
|
+
):
|
|
186
|
+
try:
|
|
187
|
+
dt = datetime.strptime(txt, fmt)
|
|
188
|
+
break
|
|
189
|
+
except ValueError:
|
|
190
|
+
continue
|
|
191
|
+
if dt is None:
|
|
192
|
+
return None
|
|
193
|
+
if dt.tzinfo is None:
|
|
194
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
195
|
+
return int(dt.timestamp() * 1000)
|
|
196
|
+
|
|
197
|
+
|
|
163
198
|
class CallVisitor:
|
|
164
199
|
"""Function-call dispatch visitor methods shared across the codegen.
|
|
165
200
|
|
|
@@ -499,23 +534,22 @@ class CallVisitor:
|
|
|
499
534
|
# weekofyear(time[, tz]).
|
|
500
535
|
#
|
|
501
536
|
# Pine v6 exposes these names as BOTH variables (current bar) AND
|
|
502
|
-
# functions (arbitrary timestamp).
|
|
503
|
-
#
|
|
504
|
-
#
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
# numbers agree across both forms.
|
|
537
|
+
# functions (arbitrary timestamp). Both forms now share the same
|
|
538
|
+
# timezone-aware emission: the variable form is wired by
|
|
539
|
+
# ``BAR_BUILTINS`` in codegen/tables.py to
|
|
540
|
+
# ``tz_time_field_lambda(..., current_bar_.timestamp,
|
|
541
|
+
# syminfo_.timezone)`` and the function form below uses the same
|
|
542
|
+
# builder, so the numbers agree across both forms.
|
|
508
543
|
#
|
|
509
544
|
# Timezone handling (per Pine v6 reference docs):
|
|
510
545
|
# - Bare form ``hour(time)`` defaults its tz argument to
|
|
511
546
|
# ``syminfo.timezone`` — the SYMBOL/EXCHANGE timezone, NOT the
|
|
512
547
|
# chart's display timezone. For the corpus' ETH-USDT crypto data
|
|
513
548
|
# this is ``"UTC"`` (the ``SymInfo`` constructor default), which
|
|
514
|
-
# keeps the lambda on the cheap ``gmtime_r`` fast path
|
|
515
|
-
#
|
|
516
|
-
# ``_decompose_bar_time()`` (engine.hpp)
|
|
517
|
-
#
|
|
518
|
-
# for the same exchange TZ.
|
|
549
|
+
# keeps the lambda on the cheap ``gmtime_r`` fast path —
|
|
550
|
+
# value-identical to the engine's ``_bar_hour()`` /
|
|
551
|
+
# ``_decompose_bar_time()`` (engine.hpp) UTC helpers the variable
|
|
552
|
+
# form used to bind to.
|
|
519
553
|
#
|
|
520
554
|
# Pre-fix the harness's ``strategy_set_chart_timezone`` clobbered
|
|
521
555
|
# ``syminfo_.timezone`` with the chart display TZ, which silently
|
|
@@ -529,26 +563,16 @@ class CallVisitor:
|
|
|
529
563
|
# - Two-arg form ``hour(time, tz)`` always overrides syminfo with
|
|
530
564
|
# the explicit tz argument. Same setenv+localtime_r block as the
|
|
531
565
|
# 1-arg fallback.
|
|
532
|
-
_BAR_TIME_FUNC_EXPR = {
|
|
533
|
-
"year": "tm_buf.tm_year + 1900",
|
|
534
|
-
"month": "tm_buf.tm_mon + 1",
|
|
535
|
-
"dayofmonth": "tm_buf.tm_mday",
|
|
536
|
-
"dayofweek": "tm_buf.tm_wday + 1",
|
|
537
|
-
"hour": "tm_buf.tm_hour",
|
|
538
|
-
"minute": "tm_buf.tm_min",
|
|
539
|
-
"second": "tm_buf.tm_sec",
|
|
540
|
-
"weekofyear": "(tm_buf.tm_yday + 7 - ((tm_buf.tm_wday + 6) % 7)) / 7",
|
|
541
|
-
}
|
|
542
566
|
if (
|
|
543
567
|
namespace is None
|
|
544
|
-
and func_name in
|
|
568
|
+
and func_name in TIME_FIELD_EXPRS
|
|
545
569
|
and (node.args or node.kwargs)
|
|
546
570
|
):
|
|
547
571
|
params = sigs.get_param_names(None, func_name)
|
|
548
572
|
args = _merge_kwargs(node.args, node.kwargs, params, self._visit_expr)
|
|
549
573
|
ts_arg = args[0] if args else "current_bar_.timestamp"
|
|
550
574
|
tz_arg = args[1] if len(args) > 1 else None
|
|
551
|
-
field_expr =
|
|
575
|
+
field_expr = TIME_FIELD_EXPRS[func_name]
|
|
552
576
|
if tz_arg is None:
|
|
553
577
|
# 1-arg form — fall back to ``syminfo.timezone`` per TV
|
|
554
578
|
# docs (the EXCHANGE TZ, default "UTC" for the corpus'
|
|
@@ -559,30 +583,11 @@ class CallVisitor:
|
|
|
559
583
|
# mutex-guarded setenv+localtime_r block as the 2-arg
|
|
560
584
|
# form.
|
|
561
585
|
tz_arg = "syminfo_.timezone"
|
|
562
|
-
# 2-arg form — honor the tz argument.
|
|
563
|
-
#
|
|
564
|
-
# ``
|
|
565
|
-
#
|
|
566
|
-
return (
|
|
567
|
-
"[&]() -> int { "
|
|
568
|
-
f"std::string _tz = ({tz_arg}); "
|
|
569
|
-
f"time_t _secs = (time_t)(({ts_arg}) / 1000); "
|
|
570
|
-
"struct tm tm_buf; "
|
|
571
|
-
"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") { "
|
|
572
|
-
"gmtime_r(&_secs, &tm_buf); "
|
|
573
|
-
"} else { "
|
|
574
|
-
"static std::mutex _pf_tz_mu; "
|
|
575
|
-
"std::lock_guard<std::mutex> _pf_tz_lock(_pf_tz_mu); "
|
|
576
|
-
"const char* _old = std::getenv(\"TZ\"); "
|
|
577
|
-
"std::string _old_tz = _old ? _old : \"\"; bool _had_old = (_old != nullptr); "
|
|
578
|
-
"::setenv(\"TZ\", _tz.c_str(), 1); ::tzset(); "
|
|
579
|
-
"localtime_r(&_secs, &tm_buf); "
|
|
580
|
-
"if (_had_old) { ::setenv(\"TZ\", _old_tz.c_str(), 1); } "
|
|
581
|
-
"else { ::unsetenv(\"TZ\"); } ::tzset(); "
|
|
582
|
-
"} "
|
|
583
|
-
f"return {field_expr}; "
|
|
584
|
-
"}()"
|
|
585
|
-
)
|
|
586
|
+
# 2-arg form — honor the tz argument. The shared
|
|
587
|
+
# ``tz_time_field_lambda`` (codegen/tables.py) also backs the
|
|
588
|
+
# bare variable forms (``hour`` etc. via BAR_BUILTINS), so the
|
|
589
|
+
# numbers agree across both forms.
|
|
590
|
+
return tz_time_field_lambda(field_expr, ts_arg, tz_arg)
|
|
586
591
|
|
|
587
592
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
588
593
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
@@ -614,6 +619,43 @@ class CallVisitor:
|
|
|
614
619
|
is_tz_first = True
|
|
615
620
|
|
|
616
621
|
if is_tz_first:
|
|
622
|
+
# A single string argument is the timestamp(dateString)
|
|
623
|
+
# overload, NOT the timezone-first form. It used to fall
|
|
624
|
+
# through with year=1970 defaults — silently wrong. Pine
|
|
625
|
+
# dateString is a const string, so parse it at transpile
|
|
626
|
+
# time (common as the input.time defval); reject loudly when
|
|
627
|
+
# it is not a literal or does not parse.
|
|
628
|
+
if len(node.args) == 1:
|
|
629
|
+
if isinstance(node.args[0], StringLiteral):
|
|
630
|
+
ms = _parse_pine_datestring_ms(node.args[0].value)
|
|
631
|
+
if ms is None:
|
|
632
|
+
self._codegen_error(
|
|
633
|
+
node,
|
|
634
|
+
f"timestamp(dateString): could not parse "
|
|
635
|
+
f"'{node.args[0].value}'.",
|
|
636
|
+
hint="Supported forms: ISO-8601 "
|
|
637
|
+
"(\"2025-01-01[THH:MM:SS][±HH:MM]\") and "
|
|
638
|
+
"\"DD MMM YYYY [hh:mm[:ss]] [±HHMM]\" / "
|
|
639
|
+
"\"MMM DD YYYY ...\"; no time zone = "
|
|
640
|
+
"GMT+0.",
|
|
641
|
+
)
|
|
642
|
+
return f"{ms}LL"
|
|
643
|
+
self._codegen_error(
|
|
644
|
+
node,
|
|
645
|
+
"timestamp(dateString) requires a literal string in "
|
|
646
|
+
"PineForge (Pine v6 dateString is a const string).",
|
|
647
|
+
hint="Use a string literal, or timestamp(year, month, "
|
|
648
|
+
"day[, hour, minute, second]).",
|
|
649
|
+
)
|
|
650
|
+
# timezone-first form requires year, month, and day.
|
|
651
|
+
if len(node.args) < 4:
|
|
652
|
+
self._codegen_error(
|
|
653
|
+
node,
|
|
654
|
+
"timestamp(timezone, ...) requires year, month, and "
|
|
655
|
+
"day arguments.",
|
|
656
|
+
hint="Pine v6 signature: timestamp(timezone, year, "
|
|
657
|
+
"month, day[, hour, minute, second]).",
|
|
658
|
+
)
|
|
617
659
|
args = [self._visit_expr(a) for a in node.args]
|
|
618
660
|
tz = args[0]
|
|
619
661
|
yr = args[1] if len(args) > 1 else "1970"
|
|
@@ -655,35 +697,47 @@ class CallVisitor:
|
|
|
655
697
|
f"}}()"
|
|
656
698
|
)
|
|
657
699
|
else:
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
f"
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
f"static thread_local int64_t _last_res = -1; "
|
|
674
|
-
f"if (_last_res != -1 && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
|
|
675
|
-
f"return _last_res; "
|
|
676
|
-
f"}} "
|
|
677
|
-
f"struct tm t = {{}}; "
|
|
678
|
-
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
679
|
-
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
680
|
-
f"int64_t _res = (int64_t)timegm(&t) * 1000; "
|
|
681
|
-
f"_last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
682
|
-
f"_last_res = _res; "
|
|
683
|
-
f"return _res; "
|
|
684
|
-
f"}}()"
|
|
700
|
+
# Numeric form requires year, month, and day (hour/minute/
|
|
701
|
+
# second default to 0). Anything shorter used to emit "0".
|
|
702
|
+
merged = _merge_kwargs(
|
|
703
|
+
node.args, node.kwargs,
|
|
704
|
+
sigs.get_param_names(None, "timestamp"),
|
|
705
|
+
lambda a: a,
|
|
706
|
+
)
|
|
707
|
+
if len(merged) < 3 or any(a is None for a in merged[:3]):
|
|
708
|
+
self._codegen_error(
|
|
709
|
+
node,
|
|
710
|
+
f"timestamp(...) with {len(merged)} argument(s) is "
|
|
711
|
+
f"not supported — year, month, and day are required.",
|
|
712
|
+
hint="Pine v6 signature: timestamp(year, month, day"
|
|
713
|
+
"[, hour, minute, second]); the dateString "
|
|
714
|
+
"overload is not supported in PineForge.",
|
|
685
715
|
)
|
|
686
|
-
|
|
716
|
+
args = [self._visit_expr(a) for a in merged]
|
|
717
|
+
yr = args[0]
|
|
718
|
+
mo = args[1] if len(args) > 1 else "1"
|
|
719
|
+
dy = args[2] if len(args) > 2 else "1"
|
|
720
|
+
hr = args[3] if len(args) > 3 else "0"
|
|
721
|
+
mn = args[4] if len(args) > 4 else "0"
|
|
722
|
+
sc = args[5] if len(args) > 5 else "0"
|
|
723
|
+
return (
|
|
724
|
+
f"[&]() -> int64_t {{ "
|
|
725
|
+
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
726
|
+
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
727
|
+
f"static thread_local int _last_yr = -1, _last_mo = -1, _last_dy = -1, _last_hr = -1, _last_min = -1, _last_sc = -1; "
|
|
728
|
+
f"static thread_local int64_t _last_res = -1; "
|
|
729
|
+
f"if (_last_res != -1 && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
|
|
730
|
+
f"return _last_res; "
|
|
731
|
+
f"}} "
|
|
732
|
+
f"struct tm t = {{}}; "
|
|
733
|
+
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
734
|
+
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
735
|
+
f"int64_t _res = (int64_t)timegm(&t) * 1000; "
|
|
736
|
+
f"_last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
737
|
+
f"_last_res = _res; "
|
|
738
|
+
f"return _res; "
|
|
739
|
+
f"}}()"
|
|
740
|
+
)
|
|
687
741
|
|
|
688
742
|
# barssince() — unsupported. Defensive: support_checker rejects bare
|
|
689
743
|
# barssince(...) with a hint to use ta.barssince(...). Reaching here
|
|
@@ -696,13 +750,27 @@ class CallVisitor:
|
|
|
696
750
|
|
|
697
751
|
# Type cast functions: int(x), float(x), bool(x), string(x)
|
|
698
752
|
if func_name == "int" and namespace is None and node.args:
|
|
699
|
-
|
|
753
|
+
# Pine int(na) → na (int form). Evaluate once, propagate na via
|
|
754
|
+
# the engine's int sentinel instead of collapsing NaN to 0.
|
|
755
|
+
x = self._visit_expr(node.args[0])
|
|
756
|
+
return (f"[&](){{ double _pf_v = (double)({x}); "
|
|
757
|
+
f"return is_na(_pf_v) ? na<int>() : (int)_pf_v; }}()")
|
|
700
758
|
if func_name == "float" and namespace is None and node.args:
|
|
701
759
|
return f"(double)({self._visit_expr(node.args[0])})"
|
|
702
760
|
if func_name == "bool" and namespace is None and node.args:
|
|
703
761
|
return f"(bool)({self._visit_expr(node.args[0])})"
|
|
704
762
|
if func_name == "string" and namespace is None and node.args:
|
|
705
|
-
|
|
763
|
+
# Pine string(x) cast — same emission as str.tostring(x), with
|
|
764
|
+
# string passthrough and TV-style "true"/"false" for bools
|
|
765
|
+
# (std::to_string would reject strings / render bools as 0/1).
|
|
766
|
+
arg = node.args[0]
|
|
767
|
+
inferred = self._infer_type(arg)
|
|
768
|
+
if inferred == "std::string":
|
|
769
|
+
return self._visit_expr(arg)
|
|
770
|
+
if inferred == "bool":
|
|
771
|
+
visited = self._visit_expr(arg)
|
|
772
|
+
return f'(({visited}) ? std::string("true") : std::string("false"))'
|
|
773
|
+
return self._visit_str_call("tostring", node)
|
|
706
774
|
|
|
707
775
|
# ta.pivot_point_levels — free function, not a stateful indicator
|
|
708
776
|
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
@@ -1226,6 +1294,18 @@ class CallVisitor:
|
|
|
1226
1294
|
return f'pine_str_format_time({ts}, {fmt}, {tz})'
|
|
1227
1295
|
|
|
1228
1296
|
if func_name == "replace":
|
|
1297
|
+
if len(args) >= 4:
|
|
1298
|
+
# 4-arg form: replace the Nth occurrence (0-based, per Pine
|
|
1299
|
+
# spec). Out-of-range / negative occurrence → original string.
|
|
1300
|
+
return (
|
|
1301
|
+
f'[&](){{ std::string s={args[0]}; std::string t={args[1]}; '
|
|
1302
|
+
f'std::string r={args[2]}; int _occ=(int)({args[3]}); '
|
|
1303
|
+
f'if(t.empty()||_occ<0) return s; '
|
|
1304
|
+
f'size_t p=0; int _i=0; '
|
|
1305
|
+
f'while((p=s.find(t,p))!=std::string::npos){{ '
|
|
1306
|
+
f'if(_i==_occ){{ s.replace(p,t.length(),r); break; }} '
|
|
1307
|
+
f'p+=t.length(); _i++; }} return s; }}()'
|
|
1308
|
+
)
|
|
1229
1309
|
if len(args) >= 3:
|
|
1230
1310
|
return f'[&](){{ std::string s={args[0]}; auto p=s.find({args[1]}); if(p!=std::string::npos) s.replace(p,{args[1]}.length(),{args[2]}); return s; }}()'
|
|
1231
1311
|
return 'std::string("")'
|
|
@@ -1241,8 +1321,10 @@ class CallVisitor:
|
|
|
1241
1321
|
if func_name == "round" and len(args) == 2:
|
|
1242
1322
|
return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
|
|
1243
1323
|
if func_name == "round_to_mintick":
|
|
1324
|
+
# Engine method (engine.hpp): NaN- and mintick<=0-guarded,
|
|
1325
|
+
# unlike the previous inlined unguarded std::round.
|
|
1244
1326
|
x = args[0] if args else "0.0"
|
|
1245
|
-
return f"(
|
|
1327
|
+
return f"round_to_mintick({x})"
|
|
1246
1328
|
if func_name == "todegrees":
|
|
1247
1329
|
x = args[0] if args else "0.0"
|
|
1248
1330
|
return f"({x} * 180.0 / M_PI)"
|