opencode-pyneruntime 6.6.4__py3-none-any.whl
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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import copy
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Strategy state accessors are meaningful only in the chart context — the
|
|
7
|
+
# security child process has no strategy state of its own, so referencing
|
|
8
|
+
# these in a request.security() / request.security_lower_tf() expression is
|
|
9
|
+
# always a programmer error. We raise SyntaxError when the offending attribute
|
|
10
|
+
# access is direct (i.e. not bound through a local alias). Transitive flow
|
|
11
|
+
# analysis is intentionally skipped — at runtime the security child returns
|
|
12
|
+
# inert defaults via the `lib._script is None` guard in the strategy module,
|
|
13
|
+
# so escaped cases fail safe rather than crash.
|
|
14
|
+
_FORBIDDEN_STRATEGY_STATE_ATTRS = frozenset({
|
|
15
|
+
"equity", "eventrades", "grossloss", "grossprofit", "initial_capital",
|
|
16
|
+
"losstrades", "max_drawdown", "max_runup", "netprofit", "openprofit",
|
|
17
|
+
"position_avg_price", "position_size", "wintrades",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
# Raw price series the security child can serve WITHOUT running the script's
|
|
21
|
+
# ``main()``: every one is set straight from the bar by ``_set_lib_properties``
|
|
22
|
+
# (the derived sources hl2/hlc3/ohlc4/hlcc4 are pure functions of that bar's
|
|
23
|
+
# OHLC, also set there), so reading ``lib.<field>`` after the per-bar property
|
|
24
|
+
# set is byte-identical to what ``main()`` would have produced. A
|
|
25
|
+
# ``request.security[_lower_tf]`` whose expression is only these can take the
|
|
26
|
+
# fast path (see ``security_process.security_process_main``).
|
|
27
|
+
_OHLCV_PASSTHROUGH_FIELDS = frozenset({
|
|
28
|
+
"open", "high", "low", "close", "volume",
|
|
29
|
+
"hl2", "hlc3", "ohlc4", "hlcc4",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SecurityTransformer(ast.NodeTransformer):
|
|
34
|
+
"""
|
|
35
|
+
Transform request.security() calls into multiprocessing signal/write/read pattern.
|
|
36
|
+
|
|
37
|
+
Transforms each lib.request.security(symbol, timeframe, expression, ...) call:
|
|
38
|
+
|
|
39
|
+
1. Function start (chart context only):
|
|
40
|
+
if __active_security__ is None:
|
|
41
|
+
__sec_signal__("sec_id", symbol_expr, timeframe_expr)
|
|
42
|
+
|
|
43
|
+
2. Original call position:
|
|
44
|
+
if __active_security__ == "sec_id":
|
|
45
|
+
__sec_write__("sec_id", expression)
|
|
46
|
+
var = __sec_read__("sec_id", lib.na)
|
|
47
|
+
|
|
48
|
+
3. Function end (chart context only):
|
|
49
|
+
if __active_security__ is None:
|
|
50
|
+
__sec_wait__("sec_id")
|
|
51
|
+
|
|
52
|
+
Also creates module-level __security_contexts__ dict with metadata for each context.
|
|
53
|
+
Non-constant symbol/timeframe/lookahead values (e.g., function parameters or
|
|
54
|
+
input-derived expressions) are stored as None in __security_contexts__ and
|
|
55
|
+
resolved at runtime via __sec_signal__ arguments.
|
|
56
|
+
|
|
57
|
+
Must be applied after ImportNormalizerTransformer, before PersistentSeriesTransformer.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self._counter = 0
|
|
62
|
+
self._all_contexts: dict[str, dict[str, ast.expr]] = {}
|
|
63
|
+
self._signal_args: dict[
|
|
64
|
+
str, tuple[ast.expr | None, ast.expr | None, ast.expr | None]
|
|
65
|
+
] = {}
|
|
66
|
+
self._needs_barmerge = False
|
|
67
|
+
self._needs_ltf_unzip = False
|
|
68
|
+
self._ltf_sec_ids: set[str] = set()
|
|
69
|
+
self._module_file: str = '<script>'
|
|
70
|
+
|
|
71
|
+
def _gen_id(self) -> str:
|
|
72
|
+
# The module hash keeps sec ids unique across modules: the main script and
|
|
73
|
+
# any imported library may each have their own security calls, and their
|
|
74
|
+
# contexts are merged into one registry by the runner
|
|
75
|
+
module_hash = hashlib.sha1(self._module_file.encode()).hexdigest()[:8]
|
|
76
|
+
sec_id = f"sec\xb7{module_hash}\xb7{self._counter}"
|
|
77
|
+
self._counter += 1
|
|
78
|
+
return sec_id
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _is_security_call(node: ast.Call) -> bool:
|
|
82
|
+
"""Check if node is lib.request.security(...)."""
|
|
83
|
+
return (isinstance(node.func, ast.Attribute)
|
|
84
|
+
and node.func.attr == 'security'
|
|
85
|
+
and isinstance(node.func.value, ast.Attribute)
|
|
86
|
+
and node.func.value.attr == 'request'
|
|
87
|
+
and isinstance(node.func.value.value, ast.Name)
|
|
88
|
+
and node.func.value.value.id == 'lib')
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _is_security_lower_tf_call(node: ast.Call) -> bool:
|
|
92
|
+
"""Check if node is lib.request.security_lower_tf(...)."""
|
|
93
|
+
return (isinstance(node.func, ast.Attribute)
|
|
94
|
+
and node.func.attr == 'security_lower_tf'
|
|
95
|
+
and isinstance(node.func.value, ast.Attribute)
|
|
96
|
+
and node.func.value.attr == 'request'
|
|
97
|
+
and isinstance(node.func.value.value, ast.Name)
|
|
98
|
+
and node.func.value.value.id == 'lib')
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _extract_ltf_args(call: ast.Call) -> tuple[
|
|
102
|
+
ast.expr | None, ast.expr | None, ast.expr | None, ast.expr | None
|
|
103
|
+
]:
|
|
104
|
+
"""Extract (symbol, timeframe, expression, ignore_invalid_symbol)
|
|
105
|
+
from request.security_lower_tf() call.
|
|
106
|
+
|
|
107
|
+
Note: no gaps parameter (LTF has no gaps/lookahead).
|
|
108
|
+
"""
|
|
109
|
+
args = list(call.args)
|
|
110
|
+
kwargs = {kw.arg: kw.value for kw in call.keywords if kw.arg is not None}
|
|
111
|
+
return (
|
|
112
|
+
kwargs.get('symbol', args[0] if len(args) > 0 else None),
|
|
113
|
+
kwargs.get('timeframe', args[1] if len(args) > 1 else None),
|
|
114
|
+
kwargs.get('expression', args[2] if len(args) > 2 else None),
|
|
115
|
+
kwargs.get('ignore_invalid_symbol', args[3] if len(args) > 3 else None),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _extract_args(call: ast.Call) -> tuple[
|
|
120
|
+
ast.expr | None, ast.expr | None, ast.expr | None, ast.expr | None,
|
|
121
|
+
ast.expr | None, ast.expr | None, ast.expr | None
|
|
122
|
+
]:
|
|
123
|
+
"""Extract (symbol, timeframe, expression, gaps, lookahead,
|
|
124
|
+
ignore_invalid_symbol, currency) from request.security() call.
|
|
125
|
+
|
|
126
|
+
Positional order matches Pine v6:
|
|
127
|
+
``security(symbol, timeframe, expression, gaps, lookahead,
|
|
128
|
+
ignore_invalid_symbol, currency, ...)``.
|
|
129
|
+
"""
|
|
130
|
+
args = list(call.args)
|
|
131
|
+
kwargs = {kw.arg: kw.value for kw in call.keywords if kw.arg is not None}
|
|
132
|
+
return (
|
|
133
|
+
kwargs.get('symbol', args[0] if len(args) > 0 else None),
|
|
134
|
+
kwargs.get('timeframe', args[1] if len(args) > 1 else None),
|
|
135
|
+
kwargs.get('expression', args[2] if len(args) > 2 else None),
|
|
136
|
+
kwargs.get('gaps', args[3] if len(args) > 3 else None),
|
|
137
|
+
kwargs.get('lookahead', args[4] if len(args) > 4 else None),
|
|
138
|
+
kwargs.get('ignore_invalid_symbol', args[5] if len(args) > 5 else None),
|
|
139
|
+
kwargs.get('currency', args[6] if len(args) > 6 else None),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _walk_skip_funcs(node: ast.AST):
|
|
144
|
+
"""Walk AST nodes, skipping nested function definitions."""
|
|
145
|
+
yield node
|
|
146
|
+
for child in ast.iter_child_nodes(node):
|
|
147
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
148
|
+
continue
|
|
149
|
+
yield from SecurityTransformer._walk_skip_funcs(child)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _find_forbidden_strategy_state(expr: ast.expr) -> ast.Attribute | None:
|
|
153
|
+
"""Return the first `lib.strategy.<state_attr>` Attribute node found in
|
|
154
|
+
``expr``, or None if there is none.
|
|
155
|
+
|
|
156
|
+
Only direct attribute chains (`lib.strategy.position_size` form,
|
|
157
|
+
produced by ImportNormalizerTransformer for any of `strategy.x`,
|
|
158
|
+
`from pynecore.lib import strategy; strategy.x`, or
|
|
159
|
+
`from pynecore.lib.strategy import x`) are detected. Local aliases
|
|
160
|
+
(`ps = strategy.position_size; security(..., ps)`) are not —
|
|
161
|
+
catching those would need a def-use chain analyzer; instead the
|
|
162
|
+
runtime fallback in the strategy module returns inert defaults.
|
|
163
|
+
"""
|
|
164
|
+
for sub in ast.walk(expr):
|
|
165
|
+
if not isinstance(sub, ast.Attribute):
|
|
166
|
+
continue
|
|
167
|
+
if sub.attr not in _FORBIDDEN_STRATEGY_STATE_ATTRS:
|
|
168
|
+
continue
|
|
169
|
+
parent = sub.value
|
|
170
|
+
if (isinstance(parent, ast.Attribute) and parent.attr == 'strategy'
|
|
171
|
+
and isinstance(parent.value, ast.Name)
|
|
172
|
+
and parent.value.id == 'lib'):
|
|
173
|
+
return sub
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _is_module_level_expr(node: ast.expr) -> bool:
|
|
178
|
+
"""Check if an expression can be evaluated at module level.
|
|
179
|
+
|
|
180
|
+
Constants and lib.* attribute chains are safe. Function parameters
|
|
181
|
+
and other local variables are not.
|
|
182
|
+
"""
|
|
183
|
+
if isinstance(node, ast.Constant):
|
|
184
|
+
return True
|
|
185
|
+
if isinstance(node, ast.Attribute):
|
|
186
|
+
return SecurityTransformer._is_module_level_expr(node.value)
|
|
187
|
+
if isinstance(node, ast.Name):
|
|
188
|
+
return node.id == 'lib'
|
|
189
|
+
if isinstance(node, ast.Call):
|
|
190
|
+
return SecurityTransformer._is_module_level_expr(node.func)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _ohlcv_field(node: ast.expr) -> str | None:
|
|
195
|
+
"""Return the raw-OHLCV field name if ``node`` is a bare reference to one
|
|
196
|
+
(``close`` or ``lib.close``), else None. Conservative: only a plain
|
|
197
|
+
Name/``lib.<attr>`` matches — any computation falls through to the full
|
|
198
|
+
``main()`` path."""
|
|
199
|
+
if isinstance(node, ast.Name) and node.id in _OHLCV_PASSTHROUGH_FIELDS:
|
|
200
|
+
return node.id
|
|
201
|
+
if (isinstance(node, ast.Attribute) and node.attr in _OHLCV_PASSTHROUGH_FIELDS
|
|
202
|
+
and isinstance(node.value, ast.Name) and node.value.id == 'lib'):
|
|
203
|
+
return node.attr
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def _ohlcv_passthrough(cls, expression: ast.expr | None) -> tuple[list[str], bool] | None:
|
|
208
|
+
"""Detect a plain-OHLCV ``request.security[_lower_tf]`` expression.
|
|
209
|
+
|
|
210
|
+
Returns ``(field_names, is_tuple)`` when every element is a raw price
|
|
211
|
+
series (so the security child can serve it without running ``main()``),
|
|
212
|
+
or ``None`` otherwise. ``is_tuple`` is True for a tuple/list expression
|
|
213
|
+
(column-major arrays) and False for a scalar.
|
|
214
|
+
"""
|
|
215
|
+
if expression is None:
|
|
216
|
+
return None
|
|
217
|
+
if isinstance(expression, (ast.Tuple, ast.List)):
|
|
218
|
+
if not expression.elts:
|
|
219
|
+
return None
|
|
220
|
+
fields = [cls._ohlcv_field(e) for e in expression.elts]
|
|
221
|
+
if all(f is not None for f in fields):
|
|
222
|
+
return [f for f in fields if f is not None], True
|
|
223
|
+
return None
|
|
224
|
+
field = cls._ohlcv_field(expression)
|
|
225
|
+
if field is not None:
|
|
226
|
+
return [field], False
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# --- AST node builders ---
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _lib_na() -> ast.Attribute:
|
|
233
|
+
"""Build: lib.na"""
|
|
234
|
+
return ast.Attribute(
|
|
235
|
+
value=ast.Name(id='lib', ctx=ast.Load()),
|
|
236
|
+
attr='na', ctx=ast.Load()
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _default_gaps() -> ast.Attribute:
|
|
241
|
+
"""Build: lib.barmerge.gaps_off"""
|
|
242
|
+
return ast.Attribute(
|
|
243
|
+
value=ast.Attribute(
|
|
244
|
+
value=ast.Name(id='lib', ctx=ast.Load()),
|
|
245
|
+
attr='barmerge', ctx=ast.Load()
|
|
246
|
+
),
|
|
247
|
+
attr='gaps_off', ctx=ast.Load()
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _func_call(name: str, *args: ast.expr) -> ast.Call:
|
|
252
|
+
return ast.Call(
|
|
253
|
+
func=ast.Name(id=name, ctx=ast.Load()),
|
|
254
|
+
args=list(args), keywords=[]
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _is_none_check() -> ast.Compare:
|
|
259
|
+
"""Build: __active_security__ is None"""
|
|
260
|
+
return ast.Compare(
|
|
261
|
+
left=ast.Name(id='__active_security__', ctx=ast.Load()),
|
|
262
|
+
ops=[ast.Is()], comparators=[ast.Constant(value=None)]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _eq_check(sec_id: str) -> ast.Compare:
|
|
267
|
+
"""Build: __active_security__ == sec_id"""
|
|
268
|
+
return ast.Compare(
|
|
269
|
+
left=ast.Name(id='__active_security__', ctx=ast.Load()),
|
|
270
|
+
ops=[ast.Eq()], comparators=[ast.Constant(value=sec_id)]
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _signal_block(self, sec_ids: list[str]) -> ast.If:
|
|
274
|
+
"""Build chart-context signal block for function start.
|
|
275
|
+
|
|
276
|
+
Passes actual symbol and timeframe expressions to __sec_signal__
|
|
277
|
+
so that runtime values (e.g., function parameters) are available.
|
|
278
|
+
|
|
279
|
+
Only includes sec_ids whose symbol/timeframe are module-level
|
|
280
|
+
expressions (constants or lib.* refs). Runtime-dependent signals
|
|
281
|
+
are emitted inline before their write blocks by _transform_body.
|
|
282
|
+
"""
|
|
283
|
+
body = []
|
|
284
|
+
for s in sec_ids:
|
|
285
|
+
args: list[ast.expr] = [ast.Constant(value=s)]
|
|
286
|
+
# Lookahead is omitted here: a runtime-dependent lookahead forces the
|
|
287
|
+
# sid onto the inline-signal path (see _process_func classification).
|
|
288
|
+
sym_expr, tf_expr, _ = self._signal_args[s]
|
|
289
|
+
args.append(copy.deepcopy(sym_expr) if sym_expr is not None
|
|
290
|
+
else ast.Constant(value=None))
|
|
291
|
+
args.append(copy.deepcopy(tf_expr) if tf_expr is not None
|
|
292
|
+
else ast.Constant(value=None))
|
|
293
|
+
body.append(ast.Expr(value=self._func_call('__sec_signal__', *args)))
|
|
294
|
+
return ast.If(
|
|
295
|
+
test=self._is_none_check(),
|
|
296
|
+
body=body,
|
|
297
|
+
orelse=[]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _inline_signal(self, sec_id: str) -> ast.If:
|
|
301
|
+
"""Build a single inline signal for runtime-dependent
|
|
302
|
+
symbol/timeframe/lookahead."""
|
|
303
|
+
args: list[ast.expr] = [ast.Constant(value=sec_id)]
|
|
304
|
+
sym_expr, tf_expr, la_expr = self._signal_args[sec_id]
|
|
305
|
+
args.append(copy.deepcopy(sym_expr) if sym_expr is not None
|
|
306
|
+
else ast.Constant(value=None))
|
|
307
|
+
args.append(copy.deepcopy(tf_expr) if tf_expr is not None
|
|
308
|
+
else ast.Constant(value=None))
|
|
309
|
+
if la_expr is not None:
|
|
310
|
+
args.append(copy.deepcopy(la_expr))
|
|
311
|
+
return ast.If(
|
|
312
|
+
test=self._is_none_check(),
|
|
313
|
+
body=[ast.Expr(value=self._func_call('__sec_signal__', *args))],
|
|
314
|
+
orelse=[]
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _wait_block(self, sec_ids: list[str]) -> ast.If:
|
|
318
|
+
"""Build chart-context wait block for function end."""
|
|
319
|
+
return ast.If(
|
|
320
|
+
test=self._is_none_check(),
|
|
321
|
+
body=[
|
|
322
|
+
ast.Expr(value=self._func_call('__sec_wait__', ast.Constant(value=s)))
|
|
323
|
+
for s in sec_ids
|
|
324
|
+
],
|
|
325
|
+
orelse=[]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _in_same_context(sec_id: str) -> ast.Compare:
|
|
330
|
+
"""Build: sec_id in __same_context__"""
|
|
331
|
+
return ast.Compare(
|
|
332
|
+
left=ast.Constant(value=sec_id),
|
|
333
|
+
ops=[ast.In()],
|
|
334
|
+
comparators=[ast.Name(id='__same_context__', ctx=ast.Load())]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def _write_block(self, sec_id: str, expression: ast.expr) -> ast.If:
|
|
338
|
+
"""Build security-context write block.
|
|
339
|
+
|
|
340
|
+
The condition fires in two cases:
|
|
341
|
+
1. This IS the security process for sec_id (__active_security__ == sec_id)
|
|
342
|
+
2. This is the chart process and sec_id is same-context (sec_id in __same_context__)
|
|
343
|
+
"""
|
|
344
|
+
return ast.If(
|
|
345
|
+
test=ast.BoolOp(
|
|
346
|
+
op=ast.Or(),
|
|
347
|
+
values=[self._eq_check(sec_id), self._in_same_context(sec_id)]
|
|
348
|
+
),
|
|
349
|
+
body=[
|
|
350
|
+
ast.Expr(value=self._func_call(
|
|
351
|
+
'__sec_write__', ast.Constant(value=sec_id), expression
|
|
352
|
+
))
|
|
353
|
+
],
|
|
354
|
+
orelse=[]
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def _sec_read_call(self, sec_id: str, tuple_len: int | None = None) -> ast.Call:
|
|
358
|
+
"""Build: __sec_read__("sec_id", <default>)
|
|
359
|
+
|
|
360
|
+
Default is ``lib.na`` for scalar reads, or an N-tuple of ``lib.na``
|
|
361
|
+
when the LHS unpacks the result. Pine `request.security()` returns a
|
|
362
|
+
tuple of `na` (one per element) on no-data bars — emitting a single
|
|
363
|
+
scalar would crash tuple-unpack with ``TypeError: cannot unpack
|
|
364
|
+
non-iterable NA object`` in `gaps_on` between-period reads or after a
|
|
365
|
+
`write_na` (session gap).
|
|
366
|
+
"""
|
|
367
|
+
if tuple_len is None:
|
|
368
|
+
default: ast.expr = self._lib_na()
|
|
369
|
+
else:
|
|
370
|
+
default = ast.Tuple(
|
|
371
|
+
elts=[self._lib_na() for _ in range(tuple_len)],
|
|
372
|
+
ctx=ast.Load()
|
|
373
|
+
)
|
|
374
|
+
return self._func_call(
|
|
375
|
+
'__sec_read__', ast.Constant(value=sec_id), default
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def _sec_read_call_ltf(self, sec_id: str, arity: int | None = None) -> ast.Call:
|
|
379
|
+
"""Build the LTF read expression.
|
|
380
|
+
|
|
381
|
+
Scalar expression: ``__sec_read__("sec_id", [])`` returns the array
|
|
382
|
+
directly. Tuple expression (``arity`` set): wrap in
|
|
383
|
+
``__ltf_unzip__(__sec_read__("sec_id", []), arity)`` to transpose the
|
|
384
|
+
row-major intrabar buffer into ``arity`` column arrays.
|
|
385
|
+
"""
|
|
386
|
+
read = self._func_call(
|
|
387
|
+
'__sec_read__', ast.Constant(value=sec_id), ast.List(elts=[], ctx=ast.Load())
|
|
388
|
+
)
|
|
389
|
+
if arity is None:
|
|
390
|
+
return read
|
|
391
|
+
return self._func_call('__ltf_unzip__', read, ast.Constant(value=arity))
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def _detect_tuple_arity(stmt: ast.stmt, call: ast.Call) -> int | None:
|
|
395
|
+
"""If ``stmt`` is a tuple-unpack assignment whose RHS is exactly
|
|
396
|
+
``call``, return the LHS arity. Otherwise None.
|
|
397
|
+
|
|
398
|
+
Pine statically enforces LHS-arity == RHS-arity for tuple-returning
|
|
399
|
+
``request.security()`` (compile errors CE10239 / CE10172), so the
|
|
400
|
+
unpack target's arity is the authoritative source.
|
|
401
|
+
|
|
402
|
+
Skipped: star-unpack (``*rest``), multi-target (``a = b = sec(...)``),
|
|
403
|
+
annotated/augmented assigns, calls wrapped in a larger expression.
|
|
404
|
+
"""
|
|
405
|
+
if not isinstance(stmt, ast.Assign):
|
|
406
|
+
return None
|
|
407
|
+
if len(stmt.targets) != 1:
|
|
408
|
+
return None
|
|
409
|
+
target = stmt.targets[0]
|
|
410
|
+
if not isinstance(target, (ast.Tuple, ast.List)):
|
|
411
|
+
return None
|
|
412
|
+
if any(isinstance(e, ast.Starred) for e in target.elts):
|
|
413
|
+
return None
|
|
414
|
+
if stmt.value is not call:
|
|
415
|
+
return None
|
|
416
|
+
return len(target.elts)
|
|
417
|
+
|
|
418
|
+
# --- Collection ---
|
|
419
|
+
|
|
420
|
+
def _collect_calls(
|
|
421
|
+
self, body: list[ast.stmt]
|
|
422
|
+
) -> list[tuple[ast.Call, str, bool]]:
|
|
423
|
+
"""
|
|
424
|
+
Find all request.security() and request.security_lower_tf() calls in
|
|
425
|
+
function body, skipping nested functions. Marks each call node with
|
|
426
|
+
_sec_id attribute.
|
|
427
|
+
|
|
428
|
+
:return: List of (call_node, sec_id, is_ltf) tuples
|
|
429
|
+
"""
|
|
430
|
+
calls = []
|
|
431
|
+
for stmt in body:
|
|
432
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
433
|
+
continue
|
|
434
|
+
for node in self._walk_skip_funcs(stmt):
|
|
435
|
+
if isinstance(node, ast.Call):
|
|
436
|
+
if self._is_security_call(node):
|
|
437
|
+
sec_id = self._gen_id()
|
|
438
|
+
node._sec_id = sec_id # type: ignore[attr-defined]
|
|
439
|
+
calls.append((node, sec_id, False))
|
|
440
|
+
elif self._is_security_lower_tf_call(node):
|
|
441
|
+
sec_id = self._gen_id()
|
|
442
|
+
node._sec_id = sec_id # type: ignore[attr-defined]
|
|
443
|
+
self._ltf_sec_ids.add(sec_id)
|
|
444
|
+
calls.append((node, sec_id, True))
|
|
445
|
+
return calls
|
|
446
|
+
|
|
447
|
+
# --- Body transformation ---
|
|
448
|
+
|
|
449
|
+
def _transform_body(
|
|
450
|
+
self, body: list[ast.stmt], call_exprs: dict[str, ast.expr],
|
|
451
|
+
runtime_sec_ids: set[str]
|
|
452
|
+
) -> list[ast.stmt]:
|
|
453
|
+
"""
|
|
454
|
+
Recursively transform a body list: insert write blocks before statements
|
|
455
|
+
containing security calls, and replace calls with __sec_read__.
|
|
456
|
+
|
|
457
|
+
For runtime-dependent sec_ids (those in ``runtime_sec_ids``), also
|
|
458
|
+
emits an inline signal just before the write block, so the signal
|
|
459
|
+
runs after the symbol/timeframe variables are defined.
|
|
460
|
+
|
|
461
|
+
Algorithm:
|
|
462
|
+
1. For each statement, first recurse into compound sub-bodies
|
|
463
|
+
(this replaces calls there and removes their _sec_id markers)
|
|
464
|
+
2. Then walk the full statement — only expression-level calls remain
|
|
465
|
+
3. Insert inline signals (if runtime) + write blocks, replace calls
|
|
466
|
+
"""
|
|
467
|
+
new_body: list[ast.stmt] = []
|
|
468
|
+
replacer = _CallReplacer(self)
|
|
469
|
+
|
|
470
|
+
for stmt in body:
|
|
471
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
472
|
+
new_body.append(stmt)
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
self._recurse_subbodies(stmt, call_exprs, runtime_sec_ids)
|
|
476
|
+
|
|
477
|
+
call_nodes_here = [
|
|
478
|
+
n for n in self._walk_skip_funcs(stmt)
|
|
479
|
+
if isinstance(n, ast.Call) and hasattr(n, '_sec_id')
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
if call_nodes_here:
|
|
483
|
+
for call_node in call_nodes_here:
|
|
484
|
+
arity = self._detect_tuple_arity(stmt, call_node)
|
|
485
|
+
if arity is not None:
|
|
486
|
+
call_node._tuple_len = arity # type: ignore[attr-defined]
|
|
487
|
+
# LTF call with an opaque tuple expression (a function
|
|
488
|
+
# call, not a literal): the expression's own arity is
|
|
489
|
+
# unknowable here, but Pine enforces LHS-arity ==
|
|
490
|
+
# RHS-arity, so the unpack target is just as
|
|
491
|
+
# authoritative — wire the __ltf_unzip__ wrap from it.
|
|
492
|
+
if (getattr(call_node, '_sec_id') in self._ltf_sec_ids
|
|
493
|
+
and not hasattr(call_node, '_ltf_arity')):
|
|
494
|
+
call_node._ltf_arity = arity # type: ignore[attr-defined]
|
|
495
|
+
self._needs_ltf_unzip = True
|
|
496
|
+
|
|
497
|
+
for call_node in call_nodes_here:
|
|
498
|
+
sid = getattr(call_node, '_sec_id')
|
|
499
|
+
if sid in runtime_sec_ids:
|
|
500
|
+
new_body.append(self._inline_signal(sid))
|
|
501
|
+
expr = copy.deepcopy(call_exprs[sid])
|
|
502
|
+
expr = replacer.visit(expr)
|
|
503
|
+
new_body.append(self._write_block(sid, expr))
|
|
504
|
+
new_body.append(replacer.visit(stmt))
|
|
505
|
+
else:
|
|
506
|
+
new_body.append(stmt)
|
|
507
|
+
|
|
508
|
+
return new_body
|
|
509
|
+
|
|
510
|
+
def _recurse_subbodies(
|
|
511
|
+
self, stmt: ast.stmt, call_exprs: dict[str, ast.expr],
|
|
512
|
+
runtime_sec_ids: set[str]
|
|
513
|
+
):
|
|
514
|
+
"""Recurse into sub-bodies of compound statements."""
|
|
515
|
+
if isinstance(stmt, ast.If):
|
|
516
|
+
stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
|
|
517
|
+
stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
|
|
518
|
+
elif isinstance(stmt, (ast.For, ast.AsyncFor, ast.While)):
|
|
519
|
+
stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
|
|
520
|
+
stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
|
|
521
|
+
elif isinstance(stmt, (ast.With, ast.AsyncWith)):
|
|
522
|
+
stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
|
|
523
|
+
elif isinstance(stmt, ast.Try):
|
|
524
|
+
stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
|
|
525
|
+
for handler in stmt.handlers:
|
|
526
|
+
handler.body = self._transform_body(handler.body, call_exprs, runtime_sec_ids)
|
|
527
|
+
stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
|
|
528
|
+
stmt.finalbody = self._transform_body(stmt.finalbody, call_exprs, runtime_sec_ids)
|
|
529
|
+
elif hasattr(ast, 'TryStar') and isinstance(stmt, ast.TryStar):
|
|
530
|
+
stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
|
|
531
|
+
for handler in stmt.handlers:
|
|
532
|
+
handler.body = self._transform_body(handler.body, call_exprs, runtime_sec_ids)
|
|
533
|
+
stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
|
|
534
|
+
stmt.finalbody = self._transform_body(stmt.finalbody, call_exprs, runtime_sec_ids)
|
|
535
|
+
elif hasattr(ast, 'Match') and isinstance(stmt, ast.Match):
|
|
536
|
+
for case in stmt.cases:
|
|
537
|
+
case.body = self._transform_body(case.body, call_exprs, runtime_sec_ids)
|
|
538
|
+
|
|
539
|
+
# --- Function & module visitors ---
|
|
540
|
+
|
|
541
|
+
def _process_func(self, node: ast.FunctionDef | ast.AsyncFunctionDef):
|
|
542
|
+
"""Transform a function containing request.security() / security_lower_tf() calls."""
|
|
543
|
+
calls = self._collect_calls(node.body)
|
|
544
|
+
|
|
545
|
+
if not calls:
|
|
546
|
+
return self.generic_visit(node)
|
|
547
|
+
|
|
548
|
+
call_exprs: dict[str, ast.expr] = {}
|
|
549
|
+
sec_ids: list[str] = []
|
|
550
|
+
|
|
551
|
+
for call, sec_id, is_ltf in calls:
|
|
552
|
+
currency = None
|
|
553
|
+
lookahead = None
|
|
554
|
+
if is_ltf:
|
|
555
|
+
symbol, timeframe, expression, ignore_invalid = (
|
|
556
|
+
self._extract_ltf_args(call)
|
|
557
|
+
)
|
|
558
|
+
gaps = None
|
|
559
|
+
# A tuple/list expression makes security_lower_tf() return one
|
|
560
|
+
# array per element (column-major). The arity is authoritative
|
|
561
|
+
# from the expression itself, independent of how the result is
|
|
562
|
+
# unpacked. Mark the call so the read is wrapped in __ltf_unzip__.
|
|
563
|
+
if isinstance(expression, (ast.Tuple, ast.List)):
|
|
564
|
+
call._ltf_arity = len(expression.elts) # type: ignore[attr-defined]
|
|
565
|
+
self._needs_ltf_unzip = True
|
|
566
|
+
else:
|
|
567
|
+
symbol, timeframe, expression, gaps, lookahead, ignore_invalid, currency = (
|
|
568
|
+
self._extract_args(call)
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if expression is not None:
|
|
572
|
+
bad = self._find_forbidden_strategy_state(expression)
|
|
573
|
+
if bad is not None:
|
|
574
|
+
fn_name = 'request.security_lower_tf' if is_ltf else 'request.security'
|
|
575
|
+
raise SyntaxError(
|
|
576
|
+
f"'strategy.{bad.attr}' cannot be used as the expression "
|
|
577
|
+
f"argument of {fn_name}() — strategy state is only "
|
|
578
|
+
f"available in the chart context, not in a security "
|
|
579
|
+
f"context.",
|
|
580
|
+
(self._module_file, getattr(bad, 'lineno', 0),
|
|
581
|
+
getattr(bad, 'col_offset', 0) + 1, None)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
call_exprs[sec_id] = expression if expression is not None else self._lib_na()
|
|
585
|
+
|
|
586
|
+
# Input-derived (Pine "simple") lookahead — e.g. the standard TV
|
|
587
|
+
# non-repaint HTF pattern ``repaint ? lookahead_off : lookahead_on``
|
|
588
|
+
# — cannot be evaluated at module level, so it is resolved at
|
|
589
|
+
# runtime through __sec_signal__ like a deferred symbol/timeframe.
|
|
590
|
+
lookahead_rt: ast.expr | None = None
|
|
591
|
+
if lookahead is not None and not self._is_module_level_expr(lookahead):
|
|
592
|
+
lookahead_rt = copy.deepcopy(lookahead)
|
|
593
|
+
|
|
594
|
+
# Store actual expressions for __sec_signal__ args (always passed at runtime)
|
|
595
|
+
self._signal_args[sec_id] = (
|
|
596
|
+
copy.deepcopy(symbol) if symbol is not None else None,
|
|
597
|
+
copy.deepcopy(timeframe) if timeframe is not None else None,
|
|
598
|
+
lookahead_rt,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# For __security_contexts__ at module level: only use values that are
|
|
602
|
+
# evaluable at module scope. Function parameters etc. become None.
|
|
603
|
+
ctx: dict[str, ast.expr] = {}
|
|
604
|
+
if symbol is not None:
|
|
605
|
+
if self._is_module_level_expr(symbol):
|
|
606
|
+
ctx['symbol'] = copy.deepcopy(symbol)
|
|
607
|
+
else:
|
|
608
|
+
ctx['symbol'] = ast.Constant(value=None)
|
|
609
|
+
if timeframe is not None:
|
|
610
|
+
if self._is_module_level_expr(timeframe):
|
|
611
|
+
ctx['timeframe'] = copy.deepcopy(timeframe)
|
|
612
|
+
else:
|
|
613
|
+
ctx['timeframe'] = ast.Constant(value=None)
|
|
614
|
+
|
|
615
|
+
if is_ltf:
|
|
616
|
+
ctx['is_ltf'] = ast.Constant(value=True)
|
|
617
|
+
else:
|
|
618
|
+
gaps_expr = (
|
|
619
|
+
copy.deepcopy(gaps) if gaps is not None else self._default_gaps()
|
|
620
|
+
)
|
|
621
|
+
ctx['gaps'] = gaps_expr
|
|
622
|
+
if lookahead is not None:
|
|
623
|
+
# A module-level lookahead (``barmerge.lookahead_*``) is
|
|
624
|
+
# consumed at module load to wire the HTF transport. An
|
|
625
|
+
# input-derived expression would NameError at import time,
|
|
626
|
+
# so store None as a placeholder — the runtime value passed
|
|
627
|
+
# to ``__sec_signal__`` resolves the mode on the first bar.
|
|
628
|
+
ctx['lookahead'] = (copy.deepcopy(lookahead) if lookahead_rt is None
|
|
629
|
+
else ast.Constant(value=None))
|
|
630
|
+
|
|
631
|
+
if ignore_invalid is not None:
|
|
632
|
+
ctx['ignore_invalid_symbol'] = copy.deepcopy(ignore_invalid)
|
|
633
|
+
|
|
634
|
+
if not is_ltf and currency is not None:
|
|
635
|
+
ctx['currency'] = copy.deepcopy(currency)
|
|
636
|
+
|
|
637
|
+
# Plain-OHLCV fast path: when the expression is only raw price
|
|
638
|
+
# series, the security child can serve it straight from each bar
|
|
639
|
+
# without running main() (see security_process.security_process_main).
|
|
640
|
+
passthrough = self._ohlcv_passthrough(expression)
|
|
641
|
+
if passthrough is not None:
|
|
642
|
+
fields, is_tuple = passthrough
|
|
643
|
+
ctx['ohlcv_fields'] = ast.List(
|
|
644
|
+
elts=[ast.Constant(value=f) for f in fields], ctx=ast.Load()
|
|
645
|
+
)
|
|
646
|
+
ctx['ohlcv_tuple'] = ast.Constant(value=is_tuple)
|
|
647
|
+
|
|
648
|
+
# Track if barmerge is used (only for non-LTF)
|
|
649
|
+
if not is_ltf:
|
|
650
|
+
if gaps is None:
|
|
651
|
+
self._needs_barmerge = True
|
|
652
|
+
elif isinstance(gaps, ast.Attribute) and hasattr(gaps, 'value'):
|
|
653
|
+
v = gaps.value
|
|
654
|
+
if isinstance(v, ast.Attribute) and v.attr == 'barmerge':
|
|
655
|
+
self._needs_barmerge = True
|
|
656
|
+
if (lookahead is not None
|
|
657
|
+
and isinstance(lookahead, ast.Attribute)
|
|
658
|
+
and hasattr(lookahead, 'value')):
|
|
659
|
+
v = lookahead.value
|
|
660
|
+
if isinstance(v, ast.Attribute) and v.attr == 'barmerge':
|
|
661
|
+
self._needs_barmerge = True
|
|
662
|
+
|
|
663
|
+
self._all_contexts[sec_id] = ctx
|
|
664
|
+
sec_ids.append(sec_id)
|
|
665
|
+
|
|
666
|
+
# Separate module-level (constant) signals from runtime-dependent ones.
|
|
667
|
+
# Module-level signals can be emitted at function start for maximum
|
|
668
|
+
# parallelism. Runtime signals must be emitted inline, after the
|
|
669
|
+
# variables they reference have been assigned. A runtime-dependent
|
|
670
|
+
# lookahead forces the inline path too — its expression references
|
|
671
|
+
# locals that do not exist yet at function start.
|
|
672
|
+
top_sec_ids = []
|
|
673
|
+
runtime_sec_ids: set[str] = set()
|
|
674
|
+
for sid in sec_ids:
|
|
675
|
+
sym_expr, tf_expr, la_expr = self._signal_args[sid]
|
|
676
|
+
sym_ok = sym_expr is None or self._is_module_level_expr(sym_expr)
|
|
677
|
+
tf_ok = tf_expr is None or self._is_module_level_expr(tf_expr)
|
|
678
|
+
if sym_ok and tf_ok and la_expr is None:
|
|
679
|
+
top_sec_ids.append(sid)
|
|
680
|
+
else:
|
|
681
|
+
runtime_sec_ids.add(sid)
|
|
682
|
+
|
|
683
|
+
original_body = node.body
|
|
684
|
+
top_block = [self._signal_block(top_sec_ids)] if top_sec_ids else []
|
|
685
|
+
node.body = (
|
|
686
|
+
top_block
|
|
687
|
+
+ self._transform_body(original_body, call_exprs, runtime_sec_ids)
|
|
688
|
+
+ [self._wait_block(sec_ids)]
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
return self.generic_visit(node)
|
|
692
|
+
|
|
693
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
694
|
+
return self._process_func(node) # type: ignore[return-value]
|
|
695
|
+
|
|
696
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AsyncFunctionDef:
|
|
697
|
+
return self._process_func(node) # type: ignore[return-value]
|
|
698
|
+
|
|
699
|
+
def visit_Module(self, node: ast.Module) -> ast.Module:
|
|
700
|
+
self._module_file = getattr(node, '_module_file_path', '<script>')
|
|
701
|
+
node = self.generic_visit(node) # type: ignore[assignment]
|
|
702
|
+
|
|
703
|
+
if self._all_contexts:
|
|
704
|
+
# Add barmerge import if needed (SecurityTransformer runs AFTER ImportNormalizer,
|
|
705
|
+
# so we must add it ourselves)
|
|
706
|
+
if self._needs_barmerge:
|
|
707
|
+
node.body.insert(0, ast.Import(
|
|
708
|
+
names=[ast.alias(name='pynecore.lib.barmerge', asname=None)]
|
|
709
|
+
))
|
|
710
|
+
|
|
711
|
+
# __ltf_unzip__ transposes tuple-valued security_lower_tf() results.
|
|
712
|
+
if self._needs_ltf_unzip:
|
|
713
|
+
node.body.insert(0, ast.ImportFrom(
|
|
714
|
+
module='pynecore.core.security',
|
|
715
|
+
names=[ast.alias(name='__ltf_unzip__', asname=None)],
|
|
716
|
+
level=0,
|
|
717
|
+
))
|
|
718
|
+
|
|
719
|
+
node.body.append(ast.Assign(
|
|
720
|
+
targets=[ast.Name(id='__security_contexts__', ctx=ast.Store())],
|
|
721
|
+
value=ast.Dict(
|
|
722
|
+
keys=[ast.Constant(value=sid) for sid in self._all_contexts],
|
|
723
|
+
values=[
|
|
724
|
+
ast.Dict(
|
|
725
|
+
keys=[ast.Constant(value=k) for k in ctx],
|
|
726
|
+
values=list(ctx.values())
|
|
727
|
+
)
|
|
728
|
+
for ctx in self._all_contexts.values()
|
|
729
|
+
]
|
|
730
|
+
)
|
|
731
|
+
))
|
|
732
|
+
|
|
733
|
+
return node
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class _CallReplacer(ast.NodeTransformer):
|
|
737
|
+
"""Replace marked request.security() call nodes with __sec_read__() calls."""
|
|
738
|
+
|
|
739
|
+
def __init__(self, parent: SecurityTransformer):
|
|
740
|
+
self._parent = parent
|
|
741
|
+
|
|
742
|
+
# noinspection PyProtectedMember
|
|
743
|
+
def visit_Call(self, node: ast.Call) -> ast.AST:
|
|
744
|
+
node = self.generic_visit(node) # type: ignore[assignment]
|
|
745
|
+
if hasattr(node, '_sec_id'):
|
|
746
|
+
sec_id = getattr(node, '_sec_id')
|
|
747
|
+
if sec_id in self._parent._ltf_sec_ids:
|
|
748
|
+
ltf_arity = getattr(node, '_ltf_arity', None)
|
|
749
|
+
return self._parent._sec_read_call_ltf(sec_id, ltf_arity)
|
|
750
|
+
tuple_len = getattr(node, '_tuple_len', None)
|
|
751
|
+
return self._parent._sec_read_call(sec_id, tuple_len)
|
|
752
|
+
return node
|