pine2pyne 0.2.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.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""`when=` of a strategy call, lowered to an `if` (card #117).
|
|
2
|
+
|
|
3
|
+
strategy.entry("l", strategy.long, when=c) if c
|
|
4
|
+
-> strategy.entry("l", strategy.long)
|
|
5
|
+
|
|
6
|
+
Pine v1-v5 gave the order functions of `strategy.*` a `when` argument: the call acts only on the bars where it is
|
|
7
|
+
true. Pine v6 removed it, and the runtime's `strategy.entry` / `strategy.close` / ... take none, so a `when=` that
|
|
8
|
+
reached the generated code raised `TypeError: entry() got an unexpected keyword argument 'when'` at run. The
|
|
9
|
+
lowering runs at the end of parsing, so no later stage sees one.
|
|
10
|
+
|
|
11
|
+
Only the keyword form, only a call that stands as a statement (the functions return nothing), and only in a script
|
|
12
|
+
declaring //@version 1-5. A v6 script is left as written: TradingView refuses a `when` there, so no spelling is
|
|
13
|
+
invented for it. A positional `when` (the 9th slot of `strategy.entry`, the 2nd of v4 `strategy.close`) is not
|
|
14
|
+
lowered here (card #117 follow-up).
|
|
15
|
+
"""
|
|
16
|
+
import dataclasses
|
|
17
|
+
import itertools
|
|
18
|
+
|
|
19
|
+
from .ast_nodes import (Assignment, ExpressionStatement, FuncDecl, FunctionCall, Identifier, IfStatement, IndexAccess,
|
|
20
|
+
MethodCall, Script)
|
|
21
|
+
from .ast_walk import child_field_names, contains, is_node
|
|
22
|
+
|
|
23
|
+
STRATEGY_FUNCTIONS_WITH_WHEN = frozenset({
|
|
24
|
+
'strategy.entry', 'strategy.order', 'strategy.exit', 'strategy.close', 'strategy.close_all',
|
|
25
|
+
'strategy.cancel', 'strategy.cancel_all',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def lower_strategy_when_arguments(script: Script, pine_version: int) -> Script:
|
|
30
|
+
"""A copy of `script` with each `strategy.*(..., when=c)` statement turned into `if c` around the call
|
|
31
|
+
without `when`. A script without one, or of another version, is returned unchanged."""
|
|
32
|
+
if not 1 <= pine_version <= 5 or not contains(script, _is_call_with_when):
|
|
33
|
+
return script
|
|
34
|
+
return _WhenLowering().lowered(script)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_call_with_when(node) -> bool:
|
|
38
|
+
# a callee that is not a name (`na(x)`: a NaLiteral node) is unhashable and is never a strategy function
|
|
39
|
+
return (isinstance(node, FunctionCall) and isinstance(node.func, str) and node.func in STRATEGY_FUNCTIONS_WITH_WHEN
|
|
40
|
+
and 'when' in node.kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_stateful_or_history(node) -> bool:
|
|
44
|
+
return isinstance(node, (FunctionCall, MethodCall, IndexAccess))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _Statements(list):
|
|
48
|
+
"""The statements one statement is replaced by (a list item that expands to several)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _WhenLowering:
|
|
52
|
+
"""Pine evaluates every argument of a `strategy.*(..., when=c)` call on every bar, whatever `c` is. An argument
|
|
53
|
+
holding a call (`ta.lowest(low, 10)`, a user function) or a history index (`close[1]`) has state per bar, and
|
|
54
|
+
inside an `if` it would update only on the bars where `c` holds. So such an argument is assigned to a temporary
|
|
55
|
+
before the `if`, and the temporary is what the call inside the `if` receives."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.temporary_numbers = itertools.count(1)
|
|
59
|
+
|
|
60
|
+
def lowered(self, node):
|
|
61
|
+
children = {name: self._map(getattr(node, name)) for name in child_field_names(type(node))}
|
|
62
|
+
rebuilt = dataclasses.replace(node, **children)
|
|
63
|
+
if isinstance(rebuilt, ExpressionStatement) and _is_call_with_when(rebuilt.expr):
|
|
64
|
+
return _Statements(self._if_around(rebuilt))
|
|
65
|
+
if isinstance(rebuilt, FuncDecl) and _is_call_with_when(rebuilt.body):
|
|
66
|
+
# `f(c) => strategy.close("l", when=c)`: the one-line body is the call, made a statement
|
|
67
|
+
call = rebuilt.body
|
|
68
|
+
statement = ExpressionStatement(expr=call, line=call.line, column=call.column)
|
|
69
|
+
return dataclasses.replace(rebuilt, body=self._if_around(statement))
|
|
70
|
+
return rebuilt
|
|
71
|
+
|
|
72
|
+
def _map(self, value):
|
|
73
|
+
if isinstance(value, list):
|
|
74
|
+
items = []
|
|
75
|
+
for item in value:
|
|
76
|
+
mapped = self._map(item)
|
|
77
|
+
items.extend(mapped) if isinstance(mapped, _Statements) else items.append(mapped)
|
|
78
|
+
return items
|
|
79
|
+
if isinstance(value, tuple):
|
|
80
|
+
return tuple(self._map(item) for item in value)
|
|
81
|
+
if isinstance(value, dict):
|
|
82
|
+
return {key: self._map(item) for key, item in value.items()}
|
|
83
|
+
return self.lowered(value) if is_node(value) else value
|
|
84
|
+
|
|
85
|
+
def _if_around(self, statement: ExpressionStatement) -> list:
|
|
86
|
+
call = statement.expr
|
|
87
|
+
temporaries = []
|
|
88
|
+
args = [self._every_bar(argument, 'argument', statement, temporaries) for argument in call.args]
|
|
89
|
+
kwargs = {key: self._every_bar(value, key, statement, temporaries)
|
|
90
|
+
for key, value in call.kwargs.items() if key != 'when'}
|
|
91
|
+
without_when = dataclasses.replace(call, args=args, kwargs=kwargs)
|
|
92
|
+
guarded = IfStatement(condition=call.kwargs['when'], body=[dataclasses.replace(statement, expr=without_when)],
|
|
93
|
+
line=statement.line, column=statement.column)
|
|
94
|
+
return [*temporaries, guarded]
|
|
95
|
+
|
|
96
|
+
def _every_bar(self, argument, label, statement, temporaries):
|
|
97
|
+
if not contains(argument, _is_stateful_or_history):
|
|
98
|
+
return argument
|
|
99
|
+
name = f'_when_{label}_{next(self.temporary_numbers)}'
|
|
100
|
+
temporaries.append(Assignment(target=name, value=argument, line=statement.line, column=statement.column))
|
|
101
|
+
return Identifier(name=name, line=statement.line, column=statement.column)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Strict `and` / `or`: Pine v5 and earlier evaluate both operands (card #65).
|
|
3
|
+
|
|
4
|
+
The Pine v6 migration guide: "Pine v5 evaluates all bool expressions except for the `?:`
|
|
5
|
+
ternary operator strictly, meaning the second part of a conditional expression is always
|
|
6
|
+
evaluated, regardless of the value of the first argument"; v6 evaluates them lazily. Python's
|
|
7
|
+
`and` / `or` are lazy, so a stateful right operand (a `ta.*` call, a function with `var` or
|
|
8
|
+
history state) of a v5 script advanced its history only on the bars where it ran. Only v5 is
|
|
9
|
+
documented; v1-v4 are taken as strict like v5, and a script without a version annotation as
|
|
10
|
+
v6, the transpiler's target (as bool_is_never_na does).
|
|
11
|
+
|
|
12
|
+
The code generator emits `all((a, b, c))` / `any((a, b, c))` for a chain of one operator whose
|
|
13
|
+
operands after the first may have an effect: a tuple evaluates every element, left to right,
|
|
14
|
+
before `all` / `any` sees any. A chain whose later operands are pure (literals, names, history
|
|
15
|
+
reads, arithmetic, comparisons, `not`, `?:`, and calls of the builtins that keep no state:
|
|
16
|
+
`na`, `nz`, `math.*`, `str.*`, the strategy trade getters, collection getters) keeps the
|
|
17
|
+
Python `and` / `or`: skipping them changes nothing, and a getter run on an empty collection
|
|
18
|
+
(`strategy.opentrades.entry_price(0)` after a size check) must not start to run. A call of a
|
|
19
|
+
function the script declares itself is stateful: it may hold `var` state or call `ta.*`.
|
|
20
|
+
Every other call (`ta.*`, `request.*`, `fixnan`, a v4 `sma(...)`) is treated as stateful. This
|
|
21
|
+
module decides which.
|
|
22
|
+
"""
|
|
23
|
+
from typing import FrozenSet, List, Optional
|
|
24
|
+
|
|
25
|
+
from .ast_nodes import (BinaryOp, Expression, FunctionCall, Identifier, IndexAccess, Literal, MemberAccess,
|
|
26
|
+
MethodCall, NaLiteral, Script, TernaryOp, UnaryOp)
|
|
27
|
+
from .ast_walk import walk
|
|
28
|
+
from .pine_builtins import TA_VARIABLE_FUNCTIONS
|
|
29
|
+
|
|
30
|
+
BOOLEAN_OPERATORS = frozenset({'and', 'or'})
|
|
31
|
+
# Nodes that cannot advance a history or change a variable. A call, a bound name (`:=`) or any
|
|
32
|
+
# node not listed is taken to have an effect
|
|
33
|
+
PURE_NODE_TYPES = (Literal, NaLiteral, Identifier, MemberAccess, IndexAccess, BinaryOp, UnaryOp, TernaryOp)
|
|
34
|
+
# Builtin calls that read their arguments and keep no state, by exact name and by namespace
|
|
35
|
+
PURE_FUNCTIONS = frozenset({'na', 'nz', 'int', 'float', 'bool', 'array.size', 'array.get', 'array.includes',
|
|
36
|
+
'array.first', 'array.last', 'array.indexof', 'map.size', 'map.get', 'map.contains'})
|
|
37
|
+
PURE_NAMESPACES = ('math.', 'str.', 'string.', 'strategy.opentrades.', 'strategy.closedtrades.')
|
|
38
|
+
# `math.random` keeps its generator, `math.sum` its rolling window: both advance only on the bars that run them
|
|
39
|
+
STATEFUL_IN_PURE_NAMESPACES = frozenset({'math.random', 'math.sum'})
|
|
40
|
+
# The same getters called as methods: `values.size()`, `lookup.get(key)`
|
|
41
|
+
PURE_METHODS = frozenset({'size', 'get', 'includes', 'contains', 'first', 'last', 'indexof'})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def evaluates_and_or_strictly(script: Script) -> bool:
|
|
45
|
+
"""True for Pine v5 and earlier; a script without a version annotation is v6."""
|
|
46
|
+
version = script.version.version if script.version else ''
|
|
47
|
+
return version.isdigit() and int(version) < 6
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _dotted_name(node) -> str:
|
|
51
|
+
if isinstance(node, str):
|
|
52
|
+
return node
|
|
53
|
+
if isinstance(node, Identifier):
|
|
54
|
+
return node.name
|
|
55
|
+
if isinstance(node, MemberAccess):
|
|
56
|
+
return f'{_dotted_name(node.object)}.{node.member}'
|
|
57
|
+
return ''
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_pure_call(node, user_function_names: FrozenSet[str]) -> bool:
|
|
61
|
+
if isinstance(node, MethodCall):
|
|
62
|
+
return node.method in PURE_METHODS and node.method not in user_function_names
|
|
63
|
+
name = 'na' if isinstance(node.func, NaLiteral) else _dotted_name(node.func)
|
|
64
|
+
if name in user_function_names:
|
|
65
|
+
return False
|
|
66
|
+
if name in PURE_FUNCTIONS:
|
|
67
|
+
return True
|
|
68
|
+
return name.startswith(PURE_NAMESPACES) and name not in STATEFUL_IN_PURE_NAMESPACES
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_pure(node, user_function_names: FrozenSet[str]) -> bool:
|
|
72
|
+
if isinstance(node, (FunctionCall, MethodCall)):
|
|
73
|
+
return _is_pure_call(node, user_function_names)
|
|
74
|
+
if not isinstance(node, PURE_NODE_TYPES):
|
|
75
|
+
return False
|
|
76
|
+
# `ta.tr`, `ta.vwap`, ... are written without () but are calls with state
|
|
77
|
+
return _dotted_name(node) not in TA_VARIABLE_FUNCTIONS
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def may_have_effect(expression: Expression, user_function_names: FrozenSet[str]) -> bool:
|
|
81
|
+
"""Whether evaluating `expression` can advance a history or bind a name."""
|
|
82
|
+
return any(not _is_pure(node, user_function_names) for node in walk(expression))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _flatten(expression: Expression, operator: str, operands: List[Expression]) -> None:
|
|
86
|
+
"""The operands of a chain of one operator, left to right; a nested group of the same
|
|
87
|
+
operator joins the chain, since strict evaluation is associative."""
|
|
88
|
+
if isinstance(expression, BinaryOp) and expression.op == operator:
|
|
89
|
+
_flatten(expression.left, operator, operands)
|
|
90
|
+
_flatten(expression.right, operator, operands)
|
|
91
|
+
else:
|
|
92
|
+
operands.append(expression)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def strict_operands(expression: BinaryOp, user_function_names: FrozenSet[str]) -> Optional[List[Expression]]:
|
|
96
|
+
"""The operands to evaluate together, or None when the plain operator is enough."""
|
|
97
|
+
if expression.op not in BOOLEAN_OPERATORS:
|
|
98
|
+
return None
|
|
99
|
+
operands: List[Expression] = []
|
|
100
|
+
_flatten(expression, expression.op, operands)
|
|
101
|
+
return operands if any(may_have_effect(operand, user_function_names) for operand in operands[1:]) else None
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symbol table for tracking variable declarations and scopes.
|
|
3
|
+
|
|
4
|
+
Used by the transformer to determine variable types and manage scope.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Dict, List, Optional, Set
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VariableKind(Enum):
|
|
11
|
+
"""Classification of variable kinds in Pine Script."""
|
|
12
|
+
VAR = "var" # Persistent variable (var keyword)
|
|
13
|
+
VARIP = "varip" # Persistent intrabar variable
|
|
14
|
+
INPUT = "input" # Input parameter
|
|
15
|
+
SERIES = "series" # Series variable (default for globals)
|
|
16
|
+
SIMPLE = "simple" # Simple (non-series) variable
|
|
17
|
+
FUNCTION = "function" # Function name
|
|
18
|
+
PARAMETER = "parameter" # Function parameter
|
|
19
|
+
TYPE = "type" # User-defined type (UDT) declaration
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Symbol:
|
|
23
|
+
"""Represents a symbol (variable, function) in the symbol table."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
name: str,
|
|
28
|
+
kind: VariableKind,
|
|
29
|
+
type_hint: Optional[str] = None,
|
|
30
|
+
is_global: bool = False,
|
|
31
|
+
line: int = 0,
|
|
32
|
+
column: int = 0,
|
|
33
|
+
):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.kind = kind
|
|
36
|
+
self.type_hint = type_hint
|
|
37
|
+
self.is_global = is_global
|
|
38
|
+
self.line = line
|
|
39
|
+
self.column = column
|
|
40
|
+
self.is_series = False # Will be set by type inference
|
|
41
|
+
self.is_indexed = False # True if variable is indexed with [n]
|
|
42
|
+
self.is_from_ta_function = False # True if assigned from ta.* function
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Scope:
|
|
46
|
+
"""Represents a lexical scope."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, parent: Optional['Scope'] = None):
|
|
49
|
+
self.parent = parent
|
|
50
|
+
self.symbols: Dict[str, Symbol] = {}
|
|
51
|
+
|
|
52
|
+
def define(self, symbol: Symbol) -> None:
|
|
53
|
+
"""Define a symbol in this scope."""
|
|
54
|
+
self.symbols[symbol.name] = symbol
|
|
55
|
+
|
|
56
|
+
def lookup(self, name: str) -> Optional[Symbol]:
|
|
57
|
+
"""Look up a symbol in this scope or parent scopes."""
|
|
58
|
+
if name in self.symbols:
|
|
59
|
+
return self.symbols[name]
|
|
60
|
+
if self.parent:
|
|
61
|
+
return self.parent.lookup(name)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def lookup_local(self, name: str) -> Optional[Symbol]:
|
|
65
|
+
"""Look up a symbol only in this scope (not parent)."""
|
|
66
|
+
return self.symbols.get(name)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SymbolTable:
|
|
70
|
+
"""Manages symbol definitions and scopes."""
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
self.global_scope = Scope()
|
|
74
|
+
self.current_scope = self.global_scope
|
|
75
|
+
self.builtin_symbols = self._init_builtins()
|
|
76
|
+
# UDT name -> field name -> declared (Python) type, from `type` declarations
|
|
77
|
+
self.udt_field_types: Dict[str, Dict[str, str]] = {}
|
|
78
|
+
|
|
79
|
+
def _init_builtins(self) -> Set[str]:
|
|
80
|
+
"""Initialize built-in Pine Script variables."""
|
|
81
|
+
return {
|
|
82
|
+
# Price data
|
|
83
|
+
'open', 'high', 'low', 'close', 'volume',
|
|
84
|
+
'hl2', 'hlc3', 'ohlc4', 'hlcc4',
|
|
85
|
+
# Bar data
|
|
86
|
+
'bar_index', 'time', 'timenow',
|
|
87
|
+
# Bar state
|
|
88
|
+
'barstate.isfirst', 'barstate.islast', 'barstate.ishistory',
|
|
89
|
+
'barstate.isrealtime', 'barstate.isnew', 'barstate.isconfirmed',
|
|
90
|
+
# Constants
|
|
91
|
+
'na', 'true', 'false',
|
|
92
|
+
# Color constants
|
|
93
|
+
'color.red', 'color.green', 'color.blue', 'color.white', 'color.black',
|
|
94
|
+
'color.yellow', 'color.orange', 'color.purple', 'color.gray',
|
|
95
|
+
# Plot styles
|
|
96
|
+
'plot.style_line', 'plot.style_stepline', 'plot.style_histogram',
|
|
97
|
+
'plot.style_cross', 'plot.style_area', 'plot.style_columns',
|
|
98
|
+
'plot.style_circles', 'plot.style_linebr', 'plot.style_steplinebr',
|
|
99
|
+
# Strategy constants
|
|
100
|
+
'strategy.long', 'strategy.short',
|
|
101
|
+
'strategy.commission.percent', 'strategy.commission.cash_per_contract',
|
|
102
|
+
'strategy.commission.cash_per_order',
|
|
103
|
+
'strategy.percent', 'strategy.fixed', 'strategy.cash',
|
|
104
|
+
'strategy.percent_of_equity',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def enter_scope(self) -> None:
|
|
108
|
+
"""Enter a new scope."""
|
|
109
|
+
self.current_scope = Scope(parent=self.current_scope)
|
|
110
|
+
|
|
111
|
+
def exit_scope(self) -> None:
|
|
112
|
+
"""Exit the current scope."""
|
|
113
|
+
if self.current_scope.parent:
|
|
114
|
+
self.current_scope = self.current_scope.parent
|
|
115
|
+
|
|
116
|
+
def define(self, symbol: Symbol) -> None:
|
|
117
|
+
"""Define a symbol in the current scope."""
|
|
118
|
+
self.current_scope.define(symbol)
|
|
119
|
+
|
|
120
|
+
def lookup(self, name: str) -> Optional[Symbol]:
|
|
121
|
+
"""Look up a symbol in current or parent scopes, then among the builtins.
|
|
122
|
+
|
|
123
|
+
A user declaration that reuses a builtin name shadows it (Pine allows
|
|
124
|
+
`close = bar_index * 2`), so the nearest declared symbol wins and only an
|
|
125
|
+
undeclared builtin name resolves to a freshly synthesized SERIES symbol.
|
|
126
|
+
"""
|
|
127
|
+
declared = self.current_scope.lookup(name)
|
|
128
|
+
if declared is not None:
|
|
129
|
+
return declared
|
|
130
|
+
|
|
131
|
+
if name in self.builtin_symbols:
|
|
132
|
+
return Symbol(name, VariableKind.SERIES, is_global=True)
|
|
133
|
+
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def lookup_global(self, name: str) -> Optional[Symbol]:
|
|
137
|
+
"""Look up a symbol only in global scope."""
|
|
138
|
+
return self.global_scope.lookup_local(name)
|
|
139
|
+
|
|
140
|
+
def is_builtin(self, name: str) -> bool:
|
|
141
|
+
"""Check if name is a built-in symbol."""
|
|
142
|
+
return name in self.builtin_symbols
|
|
143
|
+
|
|
144
|
+
def get_all_globals(self) -> List[Symbol]:
|
|
145
|
+
"""Get all symbols defined in global scope."""
|
|
146
|
+
return list(self.global_scope.symbols.values())
|
|
147
|
+
|
|
148
|
+
def mark_as_indexed(self, name: str) -> None:
|
|
149
|
+
"""Mark a variable as being indexed (e.g., close[1])."""
|
|
150
|
+
symbol = self.lookup(name)
|
|
151
|
+
if symbol:
|
|
152
|
+
symbol.is_indexed = True
|
|
153
|
+
symbol.is_series = True # Indexed variables are always Series
|
|
154
|
+
|
|
155
|
+
def mark_as_series(self, name: str) -> None:
|
|
156
|
+
"""Mark a variable as a Series type."""
|
|
157
|
+
symbol = self.lookup(name)
|
|
158
|
+
if symbol:
|
|
159
|
+
symbol.is_series = True
|
|
160
|
+
|
|
161
|
+
def mark_from_ta_function(self, name: str) -> None:
|
|
162
|
+
"""Mark a variable as assigned from a ta.* function."""
|
|
163
|
+
symbol = self.lookup(name)
|
|
164
|
+
if symbol:
|
|
165
|
+
symbol.is_from_ta_function = True
|
|
166
|
+
symbol.is_series = True # ta.* functions return Series
|
pine2pyne/tokens.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token type definitions for Pine Script v6 lexer.
|
|
3
|
+
"""
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TokenType(Enum):
|
|
10
|
+
"""All token types supported by the Pine Script v6 lexer."""
|
|
11
|
+
|
|
12
|
+
# Literals
|
|
13
|
+
INT_LITERAL = auto()
|
|
14
|
+
FLOAT_LITERAL = auto()
|
|
15
|
+
STRING_LITERAL = auto()
|
|
16
|
+
BOOL_LITERAL = auto()
|
|
17
|
+
COLOR_LITERAL = auto()
|
|
18
|
+
NA_LITERAL = auto()
|
|
19
|
+
|
|
20
|
+
# Identifiers and types
|
|
21
|
+
IDENTIFIER = auto()
|
|
22
|
+
TYPE_IDENTIFIER = auto()
|
|
23
|
+
|
|
24
|
+
# Keywords
|
|
25
|
+
VAR = auto()
|
|
26
|
+
VARIP = auto()
|
|
27
|
+
IF = auto()
|
|
28
|
+
ELSE = auto()
|
|
29
|
+
FOR = auto()
|
|
30
|
+
TO = auto()
|
|
31
|
+
BY = auto()
|
|
32
|
+
IN = auto()
|
|
33
|
+
WHILE = auto()
|
|
34
|
+
SWITCH = auto()
|
|
35
|
+
BREAK = auto()
|
|
36
|
+
CONTINUE = auto()
|
|
37
|
+
IMPORT = auto()
|
|
38
|
+
EXPORT = auto()
|
|
39
|
+
METHOD = auto()
|
|
40
|
+
TYPE = auto()
|
|
41
|
+
ENUM = auto()
|
|
42
|
+
NOT = auto()
|
|
43
|
+
AND = auto()
|
|
44
|
+
OR = auto()
|
|
45
|
+
FUNCTION = auto()
|
|
46
|
+
TRUE = auto()
|
|
47
|
+
FALSE = auto()
|
|
48
|
+
|
|
49
|
+
# Operators
|
|
50
|
+
ASSIGN = auto() # =
|
|
51
|
+
REASSIGN = auto() # :=
|
|
52
|
+
PLUS_ASSIGN = auto() # +=
|
|
53
|
+
MINUS_ASSIGN = auto() # -=
|
|
54
|
+
MULT_ASSIGN = auto() # *=
|
|
55
|
+
DIV_ASSIGN = auto() # /=
|
|
56
|
+
MOD_ASSIGN = auto() # %=
|
|
57
|
+
PLUS = auto() # +
|
|
58
|
+
MINUS = auto() # -
|
|
59
|
+
MULT = auto() # *
|
|
60
|
+
DIV = auto() # /
|
|
61
|
+
MOD = auto() # %
|
|
62
|
+
GT = auto() # >
|
|
63
|
+
LT = auto() # <
|
|
64
|
+
GTE = auto() # >=
|
|
65
|
+
LTE = auto() # <=
|
|
66
|
+
EQ = auto() # ==
|
|
67
|
+
NEQ = auto() # !=
|
|
68
|
+
ARROW = auto() # =>
|
|
69
|
+
TERNARY = auto() # ?
|
|
70
|
+
COLON = auto() # :
|
|
71
|
+
DOT = auto() # .
|
|
72
|
+
|
|
73
|
+
# Delimiters
|
|
74
|
+
LPAREN = auto() # (
|
|
75
|
+
RPAREN = auto() # )
|
|
76
|
+
LBRACKET = auto() # [
|
|
77
|
+
RBRACKET = auto() # ]
|
|
78
|
+
COMMA = auto() # ,
|
|
79
|
+
NEWLINE = auto() # \n
|
|
80
|
+
INDENT = auto() # Increase indentation
|
|
81
|
+
DEDENT = auto() # Decrease indentation
|
|
82
|
+
EOF = auto() # End of file
|
|
83
|
+
|
|
84
|
+
# Special
|
|
85
|
+
COMMENT = auto() # // comment
|
|
86
|
+
VERSION_ANNOTATION = auto() # //@version=6
|
|
87
|
+
COMPILER_ANNOTATION = auto() # //@description, //@function, etc.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Keywords mapping
|
|
91
|
+
KEYWORDS = {
|
|
92
|
+
'var': TokenType.VAR,
|
|
93
|
+
'varip': TokenType.VARIP,
|
|
94
|
+
'if': TokenType.IF,
|
|
95
|
+
'else': TokenType.ELSE,
|
|
96
|
+
'for': TokenType.FOR,
|
|
97
|
+
'to': TokenType.TO,
|
|
98
|
+
'by': TokenType.BY,
|
|
99
|
+
'in': TokenType.IN,
|
|
100
|
+
'while': TokenType.WHILE,
|
|
101
|
+
'switch': TokenType.SWITCH,
|
|
102
|
+
'break': TokenType.BREAK,
|
|
103
|
+
'continue': TokenType.CONTINUE,
|
|
104
|
+
'import': TokenType.IMPORT,
|
|
105
|
+
'export': TokenType.EXPORT,
|
|
106
|
+
'method': TokenType.METHOD,
|
|
107
|
+
'type': TokenType.TYPE,
|
|
108
|
+
'enum': TokenType.ENUM,
|
|
109
|
+
'not': TokenType.NOT,
|
|
110
|
+
'and': TokenType.AND,
|
|
111
|
+
'or': TokenType.OR,
|
|
112
|
+
'function': TokenType.FUNCTION,
|
|
113
|
+
'true': TokenType.TRUE,
|
|
114
|
+
'false': TokenType.FALSE,
|
|
115
|
+
'na': TokenType.NA_LITERAL,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Type identifiers (Pine Script built-in types)
|
|
119
|
+
TYPE_KEYWORDS = {
|
|
120
|
+
'int', 'float', 'bool', 'string', 'color',
|
|
121
|
+
'line', 'label', 'box', 'table', 'linefill', 'polyline',
|
|
122
|
+
'array', 'matrix', 'map',
|
|
123
|
+
'series', 'simple', 'input', 'const',
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class Token:
|
|
129
|
+
"""Represents a single token from the Pine Script source."""
|
|
130
|
+
type: TokenType
|
|
131
|
+
value: Any
|
|
132
|
+
line: int
|
|
133
|
+
column: int
|
|
134
|
+
lexeme: Optional[str] = None
|
|
135
|
+
is_double_quoted: bool = False # Track if string literal was double-quoted in source
|
|
136
|
+
|
|
137
|
+
def __repr__(self) -> str:
|
|
138
|
+
if self.lexeme:
|
|
139
|
+
return f"Token({self.type.name}, {self.value!r}, {self.line}:{self.column}, {self.lexeme!r})"
|
|
140
|
+
return f"Token({self.type.name}, {self.value!r}, {self.line}:{self.column})"
|
|
141
|
+
|
|
142
|
+
def __str__(self) -> str:
|
|
143
|
+
return self.__repr__()
|