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,76 @@
1
+ import ast
2
+
3
+
4
+ class PersistentSeriesTransformer(ast.NodeTransformer):
5
+ """
6
+ Transform PersistentSeries declarations into Persistent + Series combination.
7
+ Must be applied before PersistentTransformer and SeriesTransformer.
8
+ """
9
+
10
+ def visit_ImportFrom(self, node):
11
+ """Handle imports, only remove Series while keeping other imports"""
12
+ if node.module and node.module.startswith('pynecore'):
13
+ # Filter out Persistent from names
14
+ new_names = [name for name in node.names if name.name != 'PersistentSeries']
15
+ if not new_names:
16
+ # If no names left, remove the entire import
17
+ return None
18
+ # Create new import with remaining names
19
+ node.names = new_names
20
+ return node
21
+
22
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AST | list[ast.AnnAssign]:
23
+ """Transform PersistentSeries type annotations into separate Persistent and Series declarations"""
24
+ if hasattr(node, '_ps_transformed'):
25
+ return node
26
+
27
+ if not isinstance(node.target, ast.Name):
28
+ return node
29
+
30
+ # Check if it's a PersistentSeries type
31
+ is_persistent_series = False
32
+ series_type = None
33
+
34
+ if isinstance(node.annotation, ast.Subscript):
35
+ if (isinstance(node.annotation.value, ast.Name) and
36
+ node.annotation.value.id == 'PersistentSeries'):
37
+ is_persistent_series = True
38
+ series_type = node.annotation.slice
39
+ elif (isinstance(node.annotation, ast.Name) and
40
+ node.annotation.id == 'PersistentSeries'):
41
+ is_persistent_series = True
42
+
43
+ if not is_persistent_series:
44
+ return node
45
+
46
+ # Create two declarations
47
+ var_name = node.target.id
48
+ value = node.value
49
+
50
+ # 1. Persistent declaration
51
+ persistent_decl = ast.AnnAssign(
52
+ target=ast.Name(id=var_name, ctx=ast.Store()),
53
+ annotation=ast.Subscript(
54
+ value=ast.Name(id='Persistent', ctx=ast.Load()),
55
+ slice=series_type if series_type else ast.Name(id='float', ctx=ast.Load()),
56
+ ctx=ast.Load()
57
+ ) if series_type else ast.Name(id='Persistent', ctx=ast.Load()),
58
+ value=value,
59
+ simple=1
60
+ )
61
+ setattr(persistent_decl, "_ps_transformed", True)
62
+
63
+ # 2. Series declaration
64
+ series_decl = ast.AnnAssign(
65
+ target=ast.Name(id=var_name, ctx=ast.Store()),
66
+ annotation=ast.Subscript(
67
+ value=ast.Name(id='Series', ctx=ast.Load()),
68
+ slice=series_type if series_type else ast.Name(id='float', ctx=ast.Load()),
69
+ ctx=ast.Load()
70
+ ) if series_type else ast.Name(id='Series', ctx=ast.Load()),
71
+ value=ast.Name(id=var_name, ctx=ast.Load()),
72
+ simple=1
73
+ )
74
+ setattr(series_decl, "_ps_transformed", True)
75
+
76
+ return [persistent_decl, series_decl]
@@ -0,0 +1,97 @@
1
+ from typing import cast
2
+ import ast
3
+
4
+
5
+ class SafeConvertTransformer(ast.NodeTransformer):
6
+ """
7
+ Transformer that converts float(na) and int(na) calls to safe alternatives
8
+ that preserve Pine Script semantics.
9
+
10
+ This transformer replaces float() and int() function calls with safe_float()
11
+ and safe_int() from pynecore.core.safe_convert module, to ensure proper
12
+ handling of NA values.
13
+ """
14
+
15
+ def __init__(self):
16
+ self.has_safe_convert_import = False
17
+ self.has_convert_functions = False # Track if float()/int() is used
18
+
19
+ def visit_Call(self, node: ast.Call) -> ast.AST:
20
+ """
21
+ Visit Call nodes and transform float() and int() calls
22
+ """
23
+ # Continue normal transformation for children
24
+ self.generic_visit(node)
25
+
26
+ # Check if it's a built-in float() or int() call
27
+ if (isinstance(node.func, ast.Name) and
28
+ node.func.id in ('float', 'int')):
29
+
30
+ # Check for the builtin module
31
+ if hasattr(node.func, 'module') and getattr(node.func, 'module') == 'builtins':
32
+ return node
33
+
34
+ # Mark that we need the safe_convert import
35
+ self.has_convert_functions = True
36
+
37
+ # Transform to safe_convert.safe_float/safe_int call
38
+ return ast.Call(
39
+ func=ast.Attribute(
40
+ value=ast.Name(id='safe_convert', ctx=ast.Load()),
41
+ attr=f'safe_{node.func.id}',
42
+ ctx=ast.Load()
43
+ ),
44
+ args=node.args,
45
+ keywords=node.keywords
46
+ )
47
+
48
+ return node
49
+
50
+ def visit_Module(self, node: ast.Module) -> ast.Module:
51
+ """
52
+ Add safe_convert import if needed
53
+ """
54
+ # Process the module first
55
+ node = cast(ast.Module, self.generic_visit(node))
56
+
57
+ # Only add the import if we actually transformed any functions
58
+ if not self.has_convert_functions:
59
+ return node
60
+
61
+ # Check for existing safe_convert import
62
+ for stmt in node.body:
63
+ if isinstance(stmt, ast.ImportFrom) and stmt.module == 'pynecore.core.safe_convert':
64
+ self.has_safe_convert_import = True
65
+ # Check if it's imported as 'safe_convert'
66
+ for alias in stmt.names:
67
+ if alias.name == 'safe_convert' or alias.asname == 'safe_convert':
68
+ return node
69
+ elif isinstance(stmt, ast.ImportFrom) and stmt.module == 'pynecore.core':
70
+ for alias in stmt.names:
71
+ if alias.name == 'safe_convert':
72
+ self.has_safe_convert_import = True
73
+ return node
74
+
75
+ # Add import if needed
76
+ if not self.has_safe_convert_import:
77
+ import_stmt = ast.ImportFrom(
78
+ module='pynecore.core',
79
+ names=[ast.alias(name='safe_convert', asname=None)],
80
+ level=0
81
+ )
82
+
83
+ # Find the right position to insert import - after the docstring if it exists
84
+ insert_pos = 0
85
+ if (node.body and isinstance(node.body[0], ast.Expr) and
86
+ isinstance(cast(ast.Expr, node.body[0]).value, ast.Constant)):
87
+ insert_pos = 1
88
+
89
+ # Insert after any existing imports
90
+ while (insert_pos < len(node.body) and
91
+ (isinstance(node.body[insert_pos], ast.Import) or
92
+ isinstance(node.body[insert_pos], ast.ImportFrom))):
93
+ insert_pos += 1
94
+
95
+ node.body.insert(insert_pos, import_stmt)
96
+
97
+ return node
@@ -0,0 +1,95 @@
1
+ from _ast import Call, BinOp
2
+ from typing import cast
3
+ import ast
4
+
5
+
6
+ class SafeDivisionTransformer(ast.NodeTransformer):
7
+ """
8
+ Transformer that converts division operations to safe alternatives
9
+ that preserve Pine Script semantics.
10
+
11
+ This transformer replaces division operations (/) with safe_div()
12
+ from pynecore.core.safe_convert module, to ensure proper
13
+ handling of division by zero cases (returns NA instead of raising exception).
14
+ """
15
+
16
+ def __init__(self):
17
+ self.has_safe_convert_import = False
18
+ self.has_division_operations = False # Track if division is used
19
+
20
+ def visit_BinOp(self, node: ast.BinOp) -> Call | BinOp:
21
+ """
22
+ Visit BinOp nodes and transform division operations
23
+ """
24
+ # Continue normal transformation for children
25
+ self.generic_visit(node)
26
+
27
+ # Check if it's a division operation
28
+ if isinstance(node.op, ast.Div):
29
+ # Only transform if it's not a literal division (e.g., 1/2)
30
+ # Literal divisions are safe and should remain as is for performance
31
+ if not (isinstance(node.left, ast.Constant) and isinstance(node.right, ast.Constant)):
32
+ # Mark that we need the safe_convert import
33
+ self.has_division_operations = True
34
+
35
+ # Transform to safe_convert.safe_div call
36
+ return ast.Call(
37
+ func=ast.Attribute(
38
+ value=ast.Name(id='safe_convert', ctx=ast.Load()),
39
+ attr='safe_div',
40
+ ctx=ast.Load()
41
+ ),
42
+ args=[node.left, node.right],
43
+ keywords=[]
44
+ )
45
+
46
+ return node
47
+
48
+ def visit_Module(self, node: ast.Module) -> ast.Module:
49
+ """
50
+ Add safe_convert import if needed
51
+ """
52
+ # Process the module first
53
+ node = cast(ast.Module, self.generic_visit(node))
54
+
55
+ # Only add the import if we actually transformed any divisions
56
+ if not self.has_division_operations:
57
+ return node
58
+
59
+ # Check for existing safe_convert import
60
+ for stmt in node.body:
61
+ if isinstance(stmt, ast.ImportFrom) and stmt.module == 'pynecore.core.safe_convert':
62
+ self.has_safe_convert_import = True
63
+ # Check if it's imported as 'safe_convert'
64
+ for alias in stmt.names:
65
+ if alias.name == 'safe_convert' or alias.asname == 'safe_convert':
66
+ return node
67
+ elif isinstance(stmt, ast.ImportFrom) and stmt.module == 'pynecore.core':
68
+ for alias in stmt.names:
69
+ if alias.name == 'safe_convert':
70
+ self.has_safe_convert_import = True
71
+ return node
72
+
73
+ # Add import if needed
74
+ if not self.has_safe_convert_import:
75
+ import_stmt = ast.ImportFrom(
76
+ module='pynecore.core',
77
+ names=[ast.alias(name='safe_convert', asname=None)],
78
+ level=0
79
+ )
80
+
81
+ # Find the right position to insert import - after the docstring if it exists
82
+ insert_pos = 0
83
+ if (node.body and isinstance(node.body[0], ast.Expr) and
84
+ isinstance(cast(ast.Expr, node.body[0]).value, ast.Constant)):
85
+ insert_pos = 1
86
+
87
+ # Insert after any existing imports
88
+ while (insert_pos < len(node.body) and
89
+ (isinstance(node.body[insert_pos], ast.Import) or
90
+ isinstance(node.body[insert_pos], ast.ImportFrom))):
91
+ insert_pos += 1
92
+
93
+ node.body.insert(insert_pos, import_stmt)
94
+
95
+ return node
@@ -0,0 +1,308 @@
1
+ """
2
+ Detect broker capability requirements of a strategy script at compile time.
3
+
4
+ Scans the module AST for calls to ``strategy.entry``, ``strategy.exit``,
5
+ ``strategy.order``, ``strategy.close``, and ``strategy.close_all``, and
6
+ from the keyword arguments present at each call site deduces which
7
+ :class:`~pynecore.core.broker.models.ScriptRequirements` flags the script
8
+ needs.
9
+
10
+ The detected :class:`ScriptRequirements` is injected as the
11
+ ``_broker_requirements`` keyword of the ``@script.strategy(...)`` decorator
12
+ call on the script's ``main`` function, so the :class:`Script` object
13
+ carries the requirements at runtime — no need for a second AST pass or
14
+ metadata side channel.
15
+
16
+ Detection is **conservative**: if the keyword is syntactically present
17
+ (even with an ``na`` value), the requirement is taken to be needed. Better
18
+ to refuse to start against an under-capable exchange than to fail on the
19
+ first unexpected bar in live trading.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import ast
24
+
25
+ from pynecore.transformers.locations import fix_locations
26
+
27
+ __all__ = ['ScriptRequirementsTransformer']
28
+
29
+ # Flag names on ScriptRequirements — kept in sync with the dataclass in
30
+ # pynecore.core.broker.models.
31
+ _FLAG_MARKET = 'market_orders'
32
+ _FLAG_LIMIT = 'limit_orders'
33
+ _FLAG_STOP = 'stop_orders'
34
+ _FLAG_BRACKET = 'tp_sl_bracket'
35
+ _FLAG_TRAIL = 'trailing_stop'
36
+ _FLAG_STRATEGY_ORDER = 'strategy_order'
37
+ _FLAG_EXIT_ORDERS = 'exit_orders'
38
+ _FLAG_PARTIAL_QTY_BRACKET_EXIT = 'partial_qty_bracket_exit'
39
+ _FLAG_MAY_GO_SHORT = 'may_go_short'
40
+
41
+
42
+ def _strategy_call_name(node: ast.Call) -> str | None:
43
+ """
44
+ Return ``"entry"`` / ``"exit"`` / ``"order"`` / ``"close"`` / ``"close_all"``
45
+ if ``node`` is a call to ``(lib.)strategy.<that>``, else ``None``.
46
+
47
+ Matches both ``strategy.entry(...)`` (when the script imported
48
+ ``strategy`` directly) and ``lib.strategy.entry(...)`` (the form that
49
+ earlier transformers like ``ImportNormalizer`` may produce).
50
+ """
51
+ func = node.func
52
+ if not isinstance(func, ast.Attribute):
53
+ return None
54
+ method = func.attr
55
+ parent = func.value
56
+ # strategy.<method>
57
+ if isinstance(parent, ast.Name) and parent.id == 'strategy':
58
+ return method
59
+ # lib.strategy.<method>
60
+ if isinstance(parent, ast.Attribute) and parent.attr == 'strategy':
61
+ grandparent = parent.value
62
+ if isinstance(grandparent, ast.Name) and grandparent.id == 'lib':
63
+ return method
64
+ return None
65
+
66
+
67
+ def _kw_names(node: ast.Call) -> set[str]:
68
+ """Keyword argument names syntactically present on the call."""
69
+ return {kw.arg for kw in node.keywords if kw.arg is not None}
70
+
71
+
72
+ def _is_strategy_direction_base(expr: ast.expr) -> bool:
73
+ """
74
+ True if ``expr`` is one of the qualifier chains a direction constant
75
+ hangs off: ``strategy``, ``lib.strategy``, ``direction``,
76
+ ``strategy.direction``, or ``lib.strategy.direction``.
77
+ """
78
+ if isinstance(expr, ast.Name):
79
+ return expr.id in ('strategy', 'direction')
80
+ if isinstance(expr, ast.Attribute):
81
+ if expr.attr == 'direction':
82
+ return _is_strategy_direction_base(expr.value)
83
+ if expr.attr == 'strategy':
84
+ return isinstance(expr.value, ast.Name) and expr.value.id == 'lib'
85
+ return False
86
+
87
+
88
+ def _direction_is_constant_short(node: ast.Call) -> bool:
89
+ """
90
+ True if the ``direction`` argument of a ``strategy.entry`` /
91
+ ``strategy.order`` call is a syntactically constant short — the second
92
+ positional argument or the ``direction=`` keyword spelled as
93
+ ``strategy.short`` / ``lib.strategy.short`` / ``strategy.direction.short``
94
+ / ``lib.strategy.direction.short``.
95
+
96
+ Anything else — a variable, a conditional expression, a call — is a
97
+ *dynamic* direction: compile time cannot prove it, so the flag stays
98
+ ``False`` and the sync engine's projected-position runtime gate is the
99
+ authoritative guard on short-incapable venues.
100
+ """
101
+ expr: ast.expr | None = None
102
+ if len(node.args) >= 2:
103
+ expr = node.args[1]
104
+ for kw in node.keywords:
105
+ if kw.arg == 'direction':
106
+ expr = kw.value
107
+ if expr is None:
108
+ return False
109
+ return (isinstance(expr, ast.Attribute) and expr.attr == 'short'
110
+ and _is_strategy_direction_base(expr.value))
111
+
112
+
113
+ def _is_script_strategy_decorator(node: ast.expr) -> bool:
114
+ """
115
+ True if ``node`` is a ``@script.strategy(...)`` call — matches both the
116
+ raw form and the ``@lib.script.strategy(...)`` form produced by
117
+ :class:`ImportNormalizerTransformer`.
118
+ """
119
+ if not isinstance(node, ast.Call):
120
+ return False
121
+ func = node.func
122
+ if not (isinstance(func, ast.Attribute) and func.attr == 'strategy'):
123
+ return False
124
+ parent = func.value
125
+ # script.strategy
126
+ if isinstance(parent, ast.Name) and parent.id == 'script':
127
+ return True
128
+ # lib.script.strategy
129
+ if (isinstance(parent, ast.Attribute) and parent.attr == 'script'
130
+ and isinstance(parent.value, ast.Name) and parent.value.id == 'lib'):
131
+ return True
132
+ return False
133
+
134
+
135
+ class ScriptRequirementsTransformer(ast.NodeTransformer):
136
+ """
137
+ Compute :class:`ScriptRequirements` for a strategy script and inject it
138
+ into the ``@script.strategy(...)`` decorator as the
139
+ ``_broker_requirements`` keyword argument.
140
+
141
+ No-op on scripts that have no ``@script.strategy(...)`` decorator
142
+ (indicator scripts).
143
+ """
144
+
145
+ def __init__(self) -> None:
146
+ self._reqs: dict[str, bool] = {
147
+ _FLAG_MARKET: False,
148
+ _FLAG_LIMIT: False,
149
+ _FLAG_STOP: False,
150
+ _FLAG_BRACKET: False,
151
+ _FLAG_TRAIL: False,
152
+ _FLAG_STRATEGY_ORDER: False,
153
+ _FLAG_EXIT_ORDERS: False,
154
+ _FLAG_PARTIAL_QTY_BRACKET_EXIT: False,
155
+ _FLAG_MAY_GO_SHORT: False,
156
+ }
157
+ self._strategy_decorator: ast.Call | None = None
158
+
159
+ def visit_Module(self, node: ast.Module) -> ast.Module:
160
+ self.generic_visit(node)
161
+ if self._strategy_decorator is None:
162
+ return node
163
+ self._inject_requirements(node, self._strategy_decorator)
164
+ fix_locations(node)
165
+ return node
166
+
167
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
168
+ for dec in node.decorator_list:
169
+ if _is_script_strategy_decorator(dec):
170
+ self._strategy_decorator = dec # type: ignore[assignment]
171
+ break
172
+ self.generic_visit(node)
173
+ return node
174
+
175
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AsyncFunctionDef:
176
+ for dec in node.decorator_list:
177
+ if _is_script_strategy_decorator(dec):
178
+ self._strategy_decorator = dec # type: ignore[assignment]
179
+ break
180
+ self.generic_visit(node)
181
+ return node
182
+
183
+ def visit_Call(self, node: ast.Call) -> ast.Call:
184
+ self.generic_visit(node)
185
+ name = _strategy_call_name(node)
186
+ if name is None:
187
+ return node
188
+ kws = _kw_names(node)
189
+ if name == 'entry':
190
+ self._apply_entry_or_order(kws, is_strategy_order=False)
191
+ if _direction_is_constant_short(node):
192
+ self._reqs[_FLAG_MAY_GO_SHORT] = True
193
+ elif name == 'order':
194
+ self._apply_entry_or_order(kws, is_strategy_order=True)
195
+ if _direction_is_constant_short(node):
196
+ self._reqs[_FLAG_MAY_GO_SHORT] = True
197
+ elif name == 'exit':
198
+ self._apply_exit(kws)
199
+ self._reqs[_FLAG_EXIT_ORDERS] = True
200
+ elif name in ('close', 'close_all'):
201
+ self._reqs[_FLAG_MARKET] = True
202
+ self._reqs[_FLAG_EXIT_ORDERS] = True
203
+ return node
204
+
205
+ # === Detection rules (see design doc, "Detectable Patterns" table) ===
206
+
207
+ def _apply_entry_or_order(self, kws: set[str], *, is_strategy_order: bool) -> None:
208
+ has_limit = 'limit' in kws
209
+ has_stop = 'stop' in kws
210
+ if is_strategy_order:
211
+ self._reqs[_FLAG_STRATEGY_ORDER] = True
212
+ if has_limit and has_stop:
213
+ # Pine has no stop-limit entry: a both-set order is two OCO legs.
214
+ # The broker layer rests the LIMIT leg natively and fires the STOP
215
+ # leg as a software price-watch → MARKET order, so the script needs
216
+ # both the limit and market capabilities (not a native stop entry).
217
+ self._reqs[_FLAG_LIMIT] = True
218
+ self._reqs[_FLAG_MARKET] = True
219
+ elif has_limit:
220
+ self._reqs[_FLAG_LIMIT] = True
221
+ elif has_stop:
222
+ self._reqs[_FLAG_STOP] = True
223
+ else:
224
+ self._reqs[_FLAG_MARKET] = True
225
+
226
+ def _apply_exit(self, kws: set[str]) -> None:
227
+ has_limit = 'limit' in kws
228
+ has_stop = 'stop' in kws
229
+ has_profit_ticks = 'profit' in kws
230
+ has_loss_ticks = 'loss' in kws
231
+ has_trail = (
232
+ 'trail_offset' in kws or 'trail_price' in kws or 'trail_points' in kws
233
+ )
234
+ has_qty = 'qty' in kws
235
+ has_bracket_leg = (
236
+ has_limit or has_stop or has_profit_ticks or has_loss_ticks or has_trail
237
+ )
238
+
239
+ # Full OCA-reduce bracket (both TP and SL)
240
+ if (has_limit and has_stop) or (has_profit_ticks and has_loss_ticks):
241
+ self._reqs[_FLAG_BRACKET] = True
242
+ self._reqs[_FLAG_LIMIT] = True
243
+ self._reqs[_FLAG_STOP] = True
244
+ else:
245
+ if has_limit or has_profit_ticks:
246
+ self._reqs[_FLAG_LIMIT] = True
247
+ if has_stop or has_loss_ticks:
248
+ self._reqs[_FLAG_STOP] = True
249
+ if has_trail:
250
+ self._reqs[_FLAG_TRAIL] = True
251
+ # ``strategy.exit(qty=N, ..., limit=.../stop=.../profit=.../loss=.../
252
+ # trail_*=...)`` — the script pairs an explicit exit qty with bracket
253
+ # leg parameters. Compile-time cannot prove ``N < total entry qty``
254
+ # (the value may be a Series or a runtime expression), so we flag the
255
+ # requirement conservatively: any ``qty`` + any bracket leg → the
256
+ # validator rejects the script against exchanges that only support
257
+ # full-row position-attribute brackets (Capital.com). Scripts that
258
+ # always exit the entire row should omit ``qty=`` — that leaves the
259
+ # exchange free to cover the whole position.
260
+ if has_qty and has_bracket_leg:
261
+ self._reqs[_FLAG_PARTIAL_QTY_BRACKET_EXIT] = True
262
+
263
+ # === AST injection ===
264
+
265
+ def _inject_requirements(self, module: ast.Module, decorator: ast.Call) -> None:
266
+ """Append ``_broker_requirements=ScriptRequirements(...)`` and add an import."""
267
+ # Build: ScriptRequirements(flag=True, ...)
268
+ req_call = ast.Call(
269
+ func=ast.Name(id='ScriptRequirements', ctx=ast.Load()),
270
+ args=[],
271
+ keywords=[
272
+ ast.keyword(arg=flag, value=ast.Constant(value=value))
273
+ for flag, value in self._reqs.items() if value
274
+ ],
275
+ )
276
+ # Remove any existing _broker_requirements keyword (idempotency)
277
+ decorator.keywords = [kw for kw in decorator.keywords
278
+ if kw.arg != '_broker_requirements']
279
+ decorator.keywords.append(
280
+ ast.keyword(arg='_broker_requirements', value=req_call)
281
+ )
282
+
283
+ # Add the import if the module does not already have it. We insert
284
+ # as the first statement; ``ImportLifter`` runs before us, so any
285
+ # docstring is still the zeroth statement.
286
+ if not self._has_script_requirements_import(module):
287
+ import_node = ast.ImportFrom(
288
+ module='pynecore.core.broker.models',
289
+ names=[ast.alias(name='ScriptRequirements', asname=None)],
290
+ level=0,
291
+ )
292
+ # Insert after the module docstring (if any) to keep it valid
293
+ insert_at = 0
294
+ first = module.body[0] if module.body else None
295
+ if (isinstance(first, ast.Expr)
296
+ and isinstance(first.value, ast.Constant)
297
+ and isinstance(first.value.value, str)):
298
+ insert_at = 1
299
+ module.body.insert(insert_at, import_node)
300
+
301
+ @staticmethod
302
+ def _has_script_requirements_import(module: ast.Module) -> bool:
303
+ for stmt in module.body:
304
+ if isinstance(stmt, ast.ImportFrom) and stmt.module == 'pynecore.core.broker.models':
305
+ for alias in stmt.names:
306
+ if alias.name == 'ScriptRequirements':
307
+ return True
308
+ return False