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,61 @@
|
|
|
1
|
+
from typing import List, cast
|
|
2
|
+
import ast
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ImportLifterTransformer(ast.NodeTransformer):
|
|
6
|
+
"""
|
|
7
|
+
AST transformer that lifts all pynecore.lib related imports to module level.
|
|
8
|
+
Does not transform the imports, just moves them to global scope.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.lifted_imports: List[ast.ImportFrom] = []
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def _is_lib_import(node: ast.ImportFrom) -> bool:
|
|
16
|
+
"""Check if an import is lib-related"""
|
|
17
|
+
return bool(node.module and
|
|
18
|
+
(node.module == 'pynecore.lib' or
|
|
19
|
+
node.module.startswith('pynecore.lib.')))
|
|
20
|
+
|
|
21
|
+
def visit_Module(self, node: ast.Module) -> ast.Module:
|
|
22
|
+
"""Process module and add lifted imports at the top"""
|
|
23
|
+
# Process the entire module first to collect all imports
|
|
24
|
+
node = cast(ast.Module, self.generic_visit(node))
|
|
25
|
+
|
|
26
|
+
# No imports were lifted, return original
|
|
27
|
+
if not self.lifted_imports:
|
|
28
|
+
return node
|
|
29
|
+
|
|
30
|
+
# Insert lifted imports after docstring if exists
|
|
31
|
+
insert_pos = 1 if (node.body and isinstance(node.body[0], ast.Expr) and
|
|
32
|
+
isinstance(cast(ast.Expr, node.body[0]).value, ast.Constant)) else 0
|
|
33
|
+
|
|
34
|
+
node.body[insert_pos:insert_pos] = self.lifted_imports
|
|
35
|
+
return node
|
|
36
|
+
|
|
37
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
38
|
+
"""Process function definitions and lift lib imports"""
|
|
39
|
+
# Process function body first
|
|
40
|
+
node = cast(ast.FunctionDef, self.generic_visit(node))
|
|
41
|
+
|
|
42
|
+
# Collect lib imports and remove them from function body
|
|
43
|
+
new_body = []
|
|
44
|
+
for stmt in node.body:
|
|
45
|
+
if isinstance(stmt, ast.ImportFrom) and self._is_lib_import(stmt):
|
|
46
|
+
# Add to lifted imports if not already there
|
|
47
|
+
if not any(self._imports_equal(stmt, lifted) for lifted in self.lifted_imports):
|
|
48
|
+
self.lifted_imports.append(stmt)
|
|
49
|
+
else:
|
|
50
|
+
new_body.append(stmt)
|
|
51
|
+
|
|
52
|
+
node.body = new_body
|
|
53
|
+
return node
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _imports_equal(import1: ast.ImportFrom, import2: ast.ImportFrom) -> bool:
|
|
57
|
+
"""Compare two import statements for equality"""
|
|
58
|
+
return (import1.module == import2.module and
|
|
59
|
+
len(import1.names) == len(import2.names) and
|
|
60
|
+
all(n1.name == n2.name and n1.asname == n2.asname
|
|
61
|
+
for n1, n2 in zip(import1.names, import2.names)))
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Dict, Set, List, Optional, cast
|
|
3
|
+
|
|
4
|
+
NON_MODULE_ATTRS = {
|
|
5
|
+
'input', # class
|
|
6
|
+
'script', # class
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImportNormalizerTransformer(ast.NodeTransformer):
|
|
11
|
+
"""
|
|
12
|
+
AST transformer that normalizes pynecore.lib imports.
|
|
13
|
+
- Converts all lib-related imports to 'from pynecore import lib'
|
|
14
|
+
- Transforms all references to use fully qualified names (lib.xxx.yyy)
|
|
15
|
+
- Moves function-level imports to module level
|
|
16
|
+
- Supports wildcard imports by using module's __all__
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
# Track import mappings: imported_name -> full_path
|
|
21
|
+
self.import_map: Dict[str, List[str]] = {}
|
|
22
|
+
# Track which names to replace with lib.xxx
|
|
23
|
+
self.names_to_replace: Set[str] = set()
|
|
24
|
+
# Track if we need to add 'from pynecore import lib'
|
|
25
|
+
self.needs_lib_import = False
|
|
26
|
+
# Track function level imports to move up
|
|
27
|
+
self.function_imports: List[ast.ImportFrom] = []
|
|
28
|
+
# Current function being processed
|
|
29
|
+
self.current_function: Optional[str] = None
|
|
30
|
+
# Track direct module imports: alias -> module_path
|
|
31
|
+
self.module_imports: Dict[str, str] = {}
|
|
32
|
+
# Track wildcard imports: module_path -> set of exposed names
|
|
33
|
+
self.wildcard_imports: Dict[str, Set[str]] = {}
|
|
34
|
+
# Track required submodules
|
|
35
|
+
self.required_submodules: Set[str] = set()
|
|
36
|
+
# Track function parameters to avoid replacing them
|
|
37
|
+
self.function_parameters: Set[str] = set()
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _is_lib_import(node: ast.ImportFrom) -> bool:
|
|
41
|
+
"""Check if an import is lib-related"""
|
|
42
|
+
return bool(node.module and (node.module == 'pynecore.lib' or
|
|
43
|
+
node.module.startswith('pynecore.lib.')))
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _is_lib_module_import(node: ast.Import) -> bool:
|
|
47
|
+
"""Check if it's a direct pynecore.lib module import"""
|
|
48
|
+
for alias in node.names:
|
|
49
|
+
if alias.name == 'pynecore.lib' or alias.name.startswith('pynecore.lib.'):
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _get_full_path(module: str, name: str) -> List[str]:
|
|
55
|
+
"""Convert module path to list of components"""
|
|
56
|
+
if module == 'pynecore.lib':
|
|
57
|
+
return ['lib', name]
|
|
58
|
+
# Handle submodules: pynecore.lib.xxx -> ['lib', 'xxx', name]
|
|
59
|
+
parts = module.split('.')
|
|
60
|
+
return ['lib'] + parts[2:] + [name]
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _get_module_all(module: str) -> Set[str]:
|
|
64
|
+
"""Get the __all__ list from a module by importing it."""
|
|
65
|
+
try:
|
|
66
|
+
# Import the module
|
|
67
|
+
imported = __import__(module, fromlist=['__all__'])
|
|
68
|
+
# Get its __all__ list
|
|
69
|
+
if hasattr(imported, '__all__'):
|
|
70
|
+
return set(imported.__all__)
|
|
71
|
+
except (ImportError, AttributeError):
|
|
72
|
+
pass
|
|
73
|
+
# Return empty set if anything goes wrong
|
|
74
|
+
return set()
|
|
75
|
+
|
|
76
|
+
def _handle_wildcard_import(self, module: str) -> None:
|
|
77
|
+
"""Process a wildcard import by recording all names from module's __all__."""
|
|
78
|
+
# Get the exposed names from the module
|
|
79
|
+
exposed = self._get_module_all(module)
|
|
80
|
+
|
|
81
|
+
if not exposed:
|
|
82
|
+
# No __all__ found, this is probably an error
|
|
83
|
+
raise SyntaxError(
|
|
84
|
+
f"Cannot use wildcard import: module {module} has no __all__ defined"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.wildcard_imports[module] = exposed
|
|
88
|
+
|
|
89
|
+
# Add all exposed names to our import mapping
|
|
90
|
+
path_parts = ['lib'] + module.split('.')[2:] # Skip 'pynecore.lib'
|
|
91
|
+
for name in exposed:
|
|
92
|
+
self.import_map[name] = path_parts + [name]
|
|
93
|
+
self.names_to_replace.add(name)
|
|
94
|
+
|
|
95
|
+
def _handle_import_from(self, node: ast.ImportFrom) -> None:
|
|
96
|
+
"""Process a lib-related 'from x import y' statement"""
|
|
97
|
+
if not self._is_lib_import(node):
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
self.needs_lib_import = True
|
|
101
|
+
module = node.module or ''
|
|
102
|
+
|
|
103
|
+
# Extract submodule if it exists
|
|
104
|
+
if module.startswith('pynecore.lib.'):
|
|
105
|
+
submodule = module.split('.')[2] # Get first part after pynecore.lib
|
|
106
|
+
self.required_submodules.add(submodule)
|
|
107
|
+
|
|
108
|
+
# Handle wildcard imports
|
|
109
|
+
for alias in node.names:
|
|
110
|
+
if alias.name == '*':
|
|
111
|
+
self._handle_wildcard_import(module)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Handle regular imports
|
|
115
|
+
for alias in node.names:
|
|
116
|
+
name = alias.name
|
|
117
|
+
asname = alias.asname or name
|
|
118
|
+
path = self._get_full_path(module, name)
|
|
119
|
+
|
|
120
|
+
self.import_map[asname] = path
|
|
121
|
+
self.names_to_replace.add(asname)
|
|
122
|
+
|
|
123
|
+
def _handle_import(self, node: ast.Import) -> None:
|
|
124
|
+
"""Process direct module imports"""
|
|
125
|
+
if not self._is_lib_module_import(node):
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
self.needs_lib_import = True
|
|
129
|
+
|
|
130
|
+
for alias in node.names:
|
|
131
|
+
if not (alias.name == 'pynecore.lib' or
|
|
132
|
+
alias.name.startswith('pynecore.lib.')):
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
parts = alias.name.split('.')
|
|
136
|
+
if len(parts) <= 2: # pynecore.lib
|
|
137
|
+
asname = alias.asname or 'lib'
|
|
138
|
+
self.module_imports[asname] = 'lib'
|
|
139
|
+
else: # pynecore.lib.xxx
|
|
140
|
+
submodule = parts[2] # First part after pynecore.lib
|
|
141
|
+
self.required_submodules.add(submodule)
|
|
142
|
+
asname = alias.asname or parts[-1]
|
|
143
|
+
self.module_imports[asname] = 'lib.' + '.'.join(parts[2:])
|
|
144
|
+
|
|
145
|
+
def visit_Module(self, node: ast.Module) -> ast.Module:
|
|
146
|
+
"""Handle module level transformations"""
|
|
147
|
+
# First collect all imports
|
|
148
|
+
has_lib_import = False
|
|
149
|
+
for stmt in node.body:
|
|
150
|
+
if isinstance(stmt, ast.ImportFrom):
|
|
151
|
+
if stmt.module == 'pynecore' and any(n.name == 'lib' for n in stmt.names):
|
|
152
|
+
has_lib_import = True
|
|
153
|
+
|
|
154
|
+
if isinstance(stmt, (ast.ImportFrom, ast.Import)):
|
|
155
|
+
self._validate_import(stmt)
|
|
156
|
+
if isinstance(stmt, ast.ImportFrom):
|
|
157
|
+
self._handle_import_from(stmt)
|
|
158
|
+
else:
|
|
159
|
+
self._handle_import(stmt)
|
|
160
|
+
|
|
161
|
+
# Process the rest of the module to collect attribute usages
|
|
162
|
+
node = cast(ast.Module, self.generic_visit(node))
|
|
163
|
+
|
|
164
|
+
# Filter out old lib imports
|
|
165
|
+
new_body = []
|
|
166
|
+
for stmt in node.body:
|
|
167
|
+
if isinstance(stmt, ast.ImportFrom):
|
|
168
|
+
if not self._is_lib_import(stmt):
|
|
169
|
+
new_body.append(stmt)
|
|
170
|
+
# Keep original lib import if exists
|
|
171
|
+
elif stmt.module == 'pynecore' and any(n.name == 'lib' for n in stmt.names):
|
|
172
|
+
new_body.append(stmt)
|
|
173
|
+
elif isinstance(stmt, ast.Import):
|
|
174
|
+
if not self._is_lib_module_import(stmt):
|
|
175
|
+
new_body.append(stmt)
|
|
176
|
+
else:
|
|
177
|
+
new_body.append(stmt)
|
|
178
|
+
|
|
179
|
+
# Add imports if needed
|
|
180
|
+
imports = []
|
|
181
|
+
|
|
182
|
+
# Add base lib import if needed and not present
|
|
183
|
+
if self.needs_lib_import and not has_lib_import:
|
|
184
|
+
imports.append(
|
|
185
|
+
ast.ImportFrom(
|
|
186
|
+
module='pynecore',
|
|
187
|
+
names=[ast.alias(name='lib', asname=None)],
|
|
188
|
+
level=0
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Add required submodule imports
|
|
193
|
+
for submodule in sorted(self.required_submodules):
|
|
194
|
+
imports.append(
|
|
195
|
+
ast.Import(
|
|
196
|
+
names=[ast.alias(name=f'pynecore.lib.{submodule}', asname=None)]
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Function level imports moved up
|
|
201
|
+
if self.function_imports:
|
|
202
|
+
imports.extend(self.function_imports)
|
|
203
|
+
|
|
204
|
+
# Insert imports after docstring if exists
|
|
205
|
+
insert_pos = 1 if (new_body and isinstance(new_body[0], ast.Expr) and
|
|
206
|
+
isinstance(cast(ast.Expr, new_body[0]).value, ast.Constant)) else 0
|
|
207
|
+
new_body[insert_pos:insert_pos] = imports
|
|
208
|
+
|
|
209
|
+
node.body = new_body
|
|
210
|
+
return node
|
|
211
|
+
|
|
212
|
+
def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
|
|
213
|
+
"""Track lib.xxx usage to detect required submodules"""
|
|
214
|
+
# Process children first
|
|
215
|
+
node = cast(ast.Attribute, self.generic_visit(node))
|
|
216
|
+
|
|
217
|
+
# Extract the full chain
|
|
218
|
+
parts = []
|
|
219
|
+
current = node
|
|
220
|
+
while isinstance(current, ast.Attribute):
|
|
221
|
+
parts.append(current.attr)
|
|
222
|
+
current = current.value
|
|
223
|
+
|
|
224
|
+
# Only process if it starts with 'lib'
|
|
225
|
+
if isinstance(current, ast.Name) and current.id == 'lib' and parts:
|
|
226
|
+
self.needs_lib_import = True
|
|
227
|
+
|
|
228
|
+
if len(parts) >= 2: # lib.x.y pattern
|
|
229
|
+
module_name = parts[-1] # Get the first part after lib
|
|
230
|
+
if module_name not in NON_MODULE_ATTRS:
|
|
231
|
+
self.required_submodules.add(module_name)
|
|
232
|
+
|
|
233
|
+
return node
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _validate_import(node: ast.ImportFrom | ast.Import) -> None:
|
|
237
|
+
"""Validate import statements for unsupported patterns."""
|
|
238
|
+
if isinstance(node, ast.ImportFrom):
|
|
239
|
+
if node.module == 'pynecore':
|
|
240
|
+
for alias in node.names:
|
|
241
|
+
if alias.name == 'lib' and alias.asname:
|
|
242
|
+
raise SyntaxError(
|
|
243
|
+
"'lib' must be imported as itself, not as an alias")
|
|
244
|
+
|
|
245
|
+
def visit_Name(self, node: ast.Name) -> ast.AST:
|
|
246
|
+
"""Transform variable references"""
|
|
247
|
+
if isinstance(node.ctx, ast.Load):
|
|
248
|
+
# Don't replace function parameters
|
|
249
|
+
if node.id in self.function_parameters:
|
|
250
|
+
return node
|
|
251
|
+
|
|
252
|
+
# Handle regular imports
|
|
253
|
+
if node.id in self.names_to_replace:
|
|
254
|
+
path = self.import_map[node.id]
|
|
255
|
+
result: ast.expr = cast(ast.expr, ast.Name(id=path[0], ctx=ast.Load()))
|
|
256
|
+
for part in path[1:]:
|
|
257
|
+
result = cast(ast.expr, ast.Attribute(
|
|
258
|
+
value=result,
|
|
259
|
+
attr=part,
|
|
260
|
+
ctx=ast.Load()
|
|
261
|
+
))
|
|
262
|
+
return result
|
|
263
|
+
# Handle module imports
|
|
264
|
+
elif node.id in self.module_imports:
|
|
265
|
+
path = self.module_imports[node.id].split('.')
|
|
266
|
+
result: ast.expr = cast(ast.expr, ast.Name(id=path[0], ctx=ast.Load()))
|
|
267
|
+
for part in path[1:]:
|
|
268
|
+
result = cast(ast.expr, ast.Attribute(
|
|
269
|
+
value=result,
|
|
270
|
+
attr=part,
|
|
271
|
+
ctx=ast.Load()
|
|
272
|
+
))
|
|
273
|
+
return result
|
|
274
|
+
# Handle names from wildcard imports
|
|
275
|
+
else:
|
|
276
|
+
# Check each wildcard imported module
|
|
277
|
+
for module, exposed in self.wildcard_imports.items():
|
|
278
|
+
if node.id in exposed:
|
|
279
|
+
path = ['lib'] + module.split('.')[2:] + [node.id]
|
|
280
|
+
result: ast.expr = cast(ast.expr, ast.Name(id=path[0], ctx=ast.Load()))
|
|
281
|
+
for part in path[1:]:
|
|
282
|
+
result = cast(ast.expr, ast.Attribute(
|
|
283
|
+
value=result,
|
|
284
|
+
attr=part,
|
|
285
|
+
ctx=ast.Load()
|
|
286
|
+
))
|
|
287
|
+
return result
|
|
288
|
+
return node
|
|
289
|
+
|
|
290
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
291
|
+
"""Process function definitions and handle imports"""
|
|
292
|
+
old_function = self.current_function
|
|
293
|
+
old_parameters = self.function_parameters.copy()
|
|
294
|
+
|
|
295
|
+
self.current_function = node.name
|
|
296
|
+
|
|
297
|
+
# Parameter default values are evaluated in the ENCLOSING scope at def-execution
|
|
298
|
+
# time, not inside the function body. Visit them before the parameter names enter
|
|
299
|
+
# `function_parameters`, otherwise a default that references a lib module shadowed
|
|
300
|
+
# by a same-named parameter (e.g. a param `position` defaulting to
|
|
301
|
+
# `position.top_center`) is left unrewritten and the lib import is dropped.
|
|
302
|
+
node.args.defaults = [cast(ast.expr, self.visit(d)) for d in node.args.defaults]
|
|
303
|
+
node.args.kw_defaults = [
|
|
304
|
+
cast(ast.expr, self.visit(d)) if d is not None else None
|
|
305
|
+
for d in node.args.kw_defaults
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
# Collect function parameters
|
|
309
|
+
for arg in node.args.args:
|
|
310
|
+
self.function_parameters.add(arg.arg)
|
|
311
|
+
|
|
312
|
+
# Also handle keyword-only args, positional-only args, vararg, and kwarg
|
|
313
|
+
for arg in node.args.posonlyargs:
|
|
314
|
+
self.function_parameters.add(arg.arg)
|
|
315
|
+
for arg in node.args.kwonlyargs:
|
|
316
|
+
self.function_parameters.add(arg.arg)
|
|
317
|
+
if node.args.vararg:
|
|
318
|
+
self.function_parameters.add(node.args.vararg.arg)
|
|
319
|
+
if node.args.kwarg:
|
|
320
|
+
self.function_parameters.add(node.args.kwarg.arg)
|
|
321
|
+
|
|
322
|
+
# Process function
|
|
323
|
+
node = cast(ast.FunctionDef, self.generic_visit(node))
|
|
324
|
+
|
|
325
|
+
# Reset function context
|
|
326
|
+
self.current_function = old_function
|
|
327
|
+
self.function_parameters = old_parameters
|
|
328
|
+
return node
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hoist ``inline_series`` calls out of lazily evaluated expression positions.
|
|
3
|
+
|
|
4
|
+
Pine evaluates a history-referenced expression (``expr[n]``) on every bar its
|
|
5
|
+
statement executes, even when the ``[n]`` sits in a ternary branch or in a
|
|
6
|
+
short-circuited ``and``/``or`` operand the bar's values skip. Verified against
|
|
7
|
+
TradingView v6 bar-by-bar: ternary branches and ``and``/``or`` operands always
|
|
8
|
+
yield fresh history; only statements inside conditionally executed BLOCKS
|
|
9
|
+
(``if`` bodies) keep the documented compressed "gap" history. PyneComp
|
|
10
|
+
compiles such history references to ``inline_series(expr, n)``, whose
|
|
11
|
+
per-anchor buffer advances only when the call site is actually reached — so
|
|
12
|
+
inside a lazy context it returns STALE history after skipped bars.
|
|
13
|
+
|
|
14
|
+
This pass hoists every ``inline_series(...)`` call found in a lazy expression
|
|
15
|
+
position to a temp assignment placed immediately before the enclosing
|
|
16
|
+
statement and replaces the call with the temp's name. The assignment stays in
|
|
17
|
+
the same statement list, so block-level conditional execution keeps its gap
|
|
18
|
+
semantics and loop bodies keep their per-iteration frequency.
|
|
19
|
+
|
|
20
|
+
``while`` tests are deliberately left untouched (they re-evaluate per
|
|
21
|
+
iteration — a hoist above the loop would freeze them), and lambdas /
|
|
22
|
+
comprehensions are not descended into (deferred or repeated evaluation).
|
|
23
|
+
"""
|
|
24
|
+
import ast
|
|
25
|
+
from typing import cast
|
|
26
|
+
|
|
27
|
+
from pynecore.transformers.locations import fix_locations
|
|
28
|
+
|
|
29
|
+
__all__ = ['InlineSeriesHoistTransformer']
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_inline_series(node: ast.expr) -> bool:
|
|
33
|
+
"""Whether a node is a direct ``inline_series(...)`` call.
|
|
34
|
+
|
|
35
|
+
:param node: Expression node to check.
|
|
36
|
+
:return: True for a plain-name ``inline_series`` call (PyneComp's emission).
|
|
37
|
+
"""
|
|
38
|
+
return (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
39
|
+
and node.func.id == 'inline_series')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ExprHoister(ast.NodeTransformer):
|
|
43
|
+
"""Rewrite one statement's expression tree: every ``inline_series`` call
|
|
44
|
+
in a lazy position is appended to ``hoisted`` as a temp assignment and
|
|
45
|
+
replaced by the temp's name.
|
|
46
|
+
|
|
47
|
+
``_lazy`` is True while the current subtree only evaluates when
|
|
48
|
+
short-circuiting (``and``/``or`` operands after the first) or ternary
|
|
49
|
+
branch selection reaches it. Inner calls are collected before outer ones,
|
|
50
|
+
so the emitted assignments are ordered definition-before-use.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, transformer: 'InlineSeriesHoistTransformer'):
|
|
54
|
+
self._transformer = transformer
|
|
55
|
+
self.hoisted: list[ast.stmt] = []
|
|
56
|
+
self._lazy = False
|
|
57
|
+
|
|
58
|
+
def visit_as(self, node: ast.expr, lazy: bool) -> ast.expr:
|
|
59
|
+
"""Visit a subtree with an explicit laziness flag.
|
|
60
|
+
|
|
61
|
+
:param node: Subtree root.
|
|
62
|
+
:param lazy: Whether the subtree is in a lazily evaluated position.
|
|
63
|
+
:return: The (possibly rewritten) subtree.
|
|
64
|
+
"""
|
|
65
|
+
prev, self._lazy = self._lazy, lazy
|
|
66
|
+
try:
|
|
67
|
+
return cast(ast.expr, self.visit(node))
|
|
68
|
+
finally:
|
|
69
|
+
self._lazy = prev
|
|
70
|
+
|
|
71
|
+
def visit_BoolOp(self, node: ast.BoolOp) -> ast.expr:
|
|
72
|
+
node.values = [self.visit_as(value, self._lazy if i == 0 else True)
|
|
73
|
+
for i, value in enumerate(node.values)]
|
|
74
|
+
return node
|
|
75
|
+
|
|
76
|
+
def visit_IfExp(self, node: ast.IfExp) -> ast.expr:
|
|
77
|
+
node.test = self.visit_as(node.test, self._lazy)
|
|
78
|
+
node.body = self.visit_as(node.body, True)
|
|
79
|
+
node.orelse = self.visit_as(node.orelse, True)
|
|
80
|
+
return node
|
|
81
|
+
|
|
82
|
+
# Deferred / repeated evaluation contexts: hoisting from inside would
|
|
83
|
+
# change how often the call runs — leave them untouched.
|
|
84
|
+
def visit_Lambda(self, node: ast.Lambda) -> ast.expr:
|
|
85
|
+
return node
|
|
86
|
+
|
|
87
|
+
def visit_ListComp(self, node: ast.ListComp) -> ast.expr:
|
|
88
|
+
return node
|
|
89
|
+
|
|
90
|
+
def visit_SetComp(self, node: ast.SetComp) -> ast.expr:
|
|
91
|
+
return node
|
|
92
|
+
|
|
93
|
+
def visit_DictComp(self, node: ast.DictComp) -> ast.expr:
|
|
94
|
+
return node
|
|
95
|
+
|
|
96
|
+
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> ast.expr:
|
|
97
|
+
return node
|
|
98
|
+
|
|
99
|
+
def visit_Call(self, node: ast.Call) -> ast.expr:
|
|
100
|
+
if not (self._lazy and _is_inline_series(node)):
|
|
101
|
+
return cast(ast.expr, self.generic_visit(node))
|
|
102
|
+
# The hoisted call runs at statement level: its arguments leave the
|
|
103
|
+
# lazy context, so nested positions are judged from an eager root
|
|
104
|
+
node.args = [self.visit_as(arg, False) for arg in node.args]
|
|
105
|
+
for keyword in node.keywords:
|
|
106
|
+
keyword.value = self.visit_as(keyword.value, False)
|
|
107
|
+
name = self._transformer.next_name()
|
|
108
|
+
self.hoisted.append(ast.Assign(
|
|
109
|
+
targets=[ast.Name(id=name, ctx=ast.Store())],
|
|
110
|
+
value=node,
|
|
111
|
+
))
|
|
112
|
+
return ast.Name(id=name, ctx=ast.Load())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class InlineSeriesHoistTransformer:
|
|
116
|
+
"""Statement-list walker applying :class:`_ExprHoister` to every
|
|
117
|
+
statement's own expressions, prepending the hoisted temp assignments in
|
|
118
|
+
the same statement list."""
|
|
119
|
+
|
|
120
|
+
#: Fields holding nested statement lists (handlers are special-cased).
|
|
121
|
+
_STMT_LIST_FIELDS = ('body', 'orelse', 'finalbody')
|
|
122
|
+
|
|
123
|
+
#: Statements whose own expressions evaluate at definition time
|
|
124
|
+
#: (decorators, defaults, bases) — nothing to hoist per bar.
|
|
125
|
+
_SKIP_EXPR_STMTS = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
126
|
+
|
|
127
|
+
def __init__(self):
|
|
128
|
+
self._counter = 0
|
|
129
|
+
|
|
130
|
+
def next_name(self) -> str:
|
|
131
|
+
"""Allocate a module-unique temp name for a hoisted call."""
|
|
132
|
+
name = f'__hist_{self._counter}__'
|
|
133
|
+
self._counter += 1
|
|
134
|
+
return name
|
|
135
|
+
|
|
136
|
+
def visit(self, tree: ast.Module) -> ast.Module:
|
|
137
|
+
"""Process a module tree in place.
|
|
138
|
+
|
|
139
|
+
:param tree: Parsed module.
|
|
140
|
+
:return: The same tree with lazy ``inline_series`` calls hoisted.
|
|
141
|
+
"""
|
|
142
|
+
tree.body = self._process_body(tree.body)
|
|
143
|
+
if self._counter:
|
|
144
|
+
fix_locations(tree)
|
|
145
|
+
return tree
|
|
146
|
+
|
|
147
|
+
def _process_body(self, body: list[ast.stmt]) -> list[ast.stmt]:
|
|
148
|
+
"""Process one statement list; returns the list with hoists inserted."""
|
|
149
|
+
new_body: list[ast.stmt] = []
|
|
150
|
+
for stmt in body:
|
|
151
|
+
# Nested statement lists first — each is its own hoist target,
|
|
152
|
+
# which keeps block-level (gap) semantics intact
|
|
153
|
+
for field in self._STMT_LIST_FIELDS:
|
|
154
|
+
value = getattr(stmt, field, None)
|
|
155
|
+
if value and isinstance(value[0], ast.stmt):
|
|
156
|
+
setattr(stmt, field, self._process_body(value))
|
|
157
|
+
for handler in getattr(stmt, 'handlers', ()):
|
|
158
|
+
handler.body = self._process_body(handler.body)
|
|
159
|
+
new_body.extend(self._hoist_from(stmt))
|
|
160
|
+
new_body.append(stmt)
|
|
161
|
+
return new_body
|
|
162
|
+
|
|
163
|
+
def _hoist_from(self, stmt: ast.stmt) -> list[ast.stmt]:
|
|
164
|
+
"""Rewrite the statement's own expression fields; return the temp
|
|
165
|
+
assignments to place before it."""
|
|
166
|
+
if isinstance(stmt, self._SKIP_EXPR_STMTS):
|
|
167
|
+
return []
|
|
168
|
+
hoister = _ExprHoister(self)
|
|
169
|
+
for field, value in ast.iter_fields(stmt):
|
|
170
|
+
if isinstance(stmt, ast.While) and field == 'test':
|
|
171
|
+
continue # re-evaluated per iteration — must stay in place
|
|
172
|
+
if isinstance(value, ast.expr):
|
|
173
|
+
setattr(stmt, field, hoister.visit_as(value, False))
|
|
174
|
+
elif (isinstance(value, list) and value
|
|
175
|
+
and isinstance(value[0], ast.expr)):
|
|
176
|
+
setattr(stmt, field,
|
|
177
|
+
[hoister.visit_as(item, False) for item in value])
|
|
178
|
+
return hoister.hoisted
|