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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. 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