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,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
|