opencode-pine2pyne 0.1.0__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 ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ pine2pyne: Pine Script v6 to PyneCore Python transpiler.
3
+
4
+ A tool to convert TradingView Pine Script indicators and strategies
5
+ to PyneCore Python code for local backtesting and analysis.
6
+ """
7
+
8
+ __version__ = '0.1.0'
9
+ __author__ = 'Pine2Pyne Transpiler'
10
+
11
+ from .lexer import Lexer, tokenize
12
+ from .parser import Parser, parse
13
+ from .transformer import Transformer, PyneTransformedScript
14
+ from .codegen import CodeGenerator, generate_code
15
+ from .errors import (
16
+ Pine2PyneError,
17
+ LexerError,
18
+ ParserError,
19
+ TransformerError,
20
+ CodeGenError,
21
+ UnsupportedFeatureError,
22
+ )
23
+
24
+
25
+ def _is_comment_only(source: str) -> bool:
26
+ """Check if source contains only comments and whitespace."""
27
+ for line in source.splitlines():
28
+ stripped = line.strip()
29
+ if stripped and not stripped.startswith('//'):
30
+ return False
31
+ return True
32
+
33
+
34
+ def transpile(source: str) -> str:
35
+ """
36
+ Transpile Pine Script source code to PyneCore Python.
37
+
38
+ Args:
39
+ source: Pine Script v6 source code
40
+
41
+ Returns:
42
+ PyneCore-compatible Python source code (empty string if source is comment-only)
43
+
44
+ Raises:
45
+ Pine2PyneError: If any stage of transpilation fails
46
+ """
47
+ # Skip empty or comment-only files
48
+ if not source or _is_comment_only(source):
49
+ return ""
50
+
51
+ # Tokenize
52
+ lexer = Lexer(source)
53
+ tokens = lexer.tokenize()
54
+
55
+ # Parse
56
+ parser = Parser(tokens)
57
+ ast = parser.parse()
58
+
59
+ # Transform
60
+ transformer = Transformer()
61
+ transformed = transformer.transform(ast)
62
+
63
+ # Generate code
64
+ codegen = CodeGenerator()
65
+ output = codegen.generate(transformed)
66
+
67
+ return output
68
+
69
+
70
+ __all__ = [
71
+ 'transpile',
72
+ 'Lexer',
73
+ 'Parser',
74
+ 'Transformer',
75
+ 'CodeGenerator',
76
+ 'tokenize',
77
+ 'parse',
78
+ 'generate_code',
79
+ 'Pine2PyneError',
80
+ 'LexerError',
81
+ 'ParserError',
82
+ 'TransformerError',
83
+ 'CodeGenError',
84
+ 'UnsupportedFeatureError',
85
+ 'PyneTransformedScript',
86
+ ]
pine2pyne/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ Entry point for running pine2pyne as a module.
3
+
4
+ Usage:
5
+ python -m pine2pyne input.pine
6
+ python -m pine2pyne input.pine -o output.py
7
+ """
8
+ import sys
9
+ from .cli import main
10
+
11
+ if __name__ == '__main__':
12
+ sys.exit(main())
pine2pyne/ast_nodes.py ADDED
@@ -0,0 +1,352 @@
1
+ """
2
+ AST (Abstract Syntax Tree) node definitions for Pine Script v6.
3
+
4
+ All node types used by the parser to represent Pine Script syntax.
5
+ """
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, List, Optional, Union
8
+
9
+
10
+ # Base class for all AST nodes
11
+ @dataclass
12
+ class ASTNode:
13
+ """Base class for all AST nodes."""
14
+ line: int = 0
15
+ column: int = 0
16
+
17
+
18
+ # ============================================================================
19
+ # Top-level nodes
20
+ # ============================================================================
21
+
22
+ @dataclass
23
+ class Script(ASTNode):
24
+ """Root node representing the entire Pine Script."""
25
+ version: Optional['VersionAnnotation'] = None
26
+ script_decl: Optional[Union['IndicatorDecl', 'StrategyDecl', 'LibraryDecl']] = None
27
+ imports: List['ImportDecl'] = field(default_factory=list)
28
+ declarations: List[Union['VarDecl', 'VaripDecl', 'TypeDecl', 'EnumDecl', 'FuncDecl', 'InputDecl']] = field(default_factory=list)
29
+ body: List['Statement'] = field(default_factory=list)
30
+
31
+
32
+ @dataclass
33
+ class VersionAnnotation(ASTNode):
34
+ """//@version=6 annotation."""
35
+ version: str = "" # "6" or "5"
36
+
37
+
38
+ @dataclass
39
+ class IndicatorDecl(ASTNode):
40
+ """indicator("Title", ...) declaration."""
41
+ title: str = ""
42
+ kwargs: dict = field(default_factory=dict)
43
+
44
+
45
+ @dataclass
46
+ class StrategyDecl(ASTNode):
47
+ """strategy("Title", ...) declaration."""
48
+ title: str = ""
49
+ kwargs: dict = field(default_factory=dict)
50
+
51
+
52
+ @dataclass
53
+ class LibraryDecl(ASTNode):
54
+ """library("Title", ...) declaration."""
55
+ title: str = ""
56
+ kwargs: dict = field(default_factory=dict)
57
+
58
+
59
+ @dataclass
60
+ class ImportDecl(ASTNode):
61
+ """import user/library/version as alias"""
62
+ user: str = ""
63
+ library: str = ""
64
+ version: Optional[str] = None
65
+ alias: Optional[str] = None
66
+
67
+
68
+ # ============================================================================
69
+ # Declarations
70
+ # ============================================================================
71
+
72
+ @dataclass
73
+ class VarDecl(ASTNode):
74
+ """var [type] name = value"""
75
+ name: str = ""
76
+ value: 'Expression' = None
77
+ type_hint: Optional[str] = None
78
+ is_var: bool = True # True for 'var', False for 'varip'
79
+
80
+
81
+ @dataclass
82
+ class VaripDecl(ASTNode):
83
+ """varip [type] name = value"""
84
+ name: str = ""
85
+ value: 'Expression' = None
86
+ type_hint: Optional[str] = None
87
+
88
+
89
+ @dataclass
90
+ class TypeDecl(ASTNode):
91
+ """type MyType ... (User-defined type)"""
92
+ name: str = ""
93
+ fields: List[tuple] = field(default_factory=list) # [(field_name, type_hint, default_value), ...] or [(field_name, type_hint), ...]
94
+
95
+
96
+ @dataclass
97
+ class EnumDecl(ASTNode):
98
+ """enum MyEnum ... (Enum definition)"""
99
+ name: str = ""
100
+ members: List = field(default_factory=list) # List[str] or List[tuple[str, Expression]]
101
+
102
+
103
+ @dataclass
104
+ class FuncDecl(ASTNode):
105
+ """Function declaration: name(params) => body"""
106
+ name: str = ""
107
+ params: List['Parameter'] = field(default_factory=list)
108
+ body: Union['Expression', List['Statement']] = None
109
+ return_type: Optional[str] = None
110
+ is_method: bool = False
111
+ is_export: bool = False
112
+ indexed_params: set = field(default_factory=set) # Params that use [n] history access
113
+
114
+
115
+ @dataclass
116
+ class Parameter(ASTNode):
117
+ """Function parameter: [type] name [= default]"""
118
+ name: str = ""
119
+ type_hint: Optional[str] = None
120
+ default: Optional['Expression'] = None
121
+
122
+
123
+ @dataclass
124
+ class InputDecl(ASTNode):
125
+ """Input declaration: name = input.*(...)"""
126
+ name: str = ""
127
+ func: str = "" # e.g., "input.int"
128
+ args: List['Expression'] = field(default_factory=list)
129
+ kwargs: dict = field(default_factory=dict)
130
+
131
+
132
+ # ============================================================================
133
+ # Expressions
134
+ # ============================================================================
135
+
136
+ @dataclass
137
+ class Expression(ASTNode):
138
+ """Base class for all expressions."""
139
+ pass
140
+
141
+
142
+ @dataclass
143
+ class BinaryOp(Expression):
144
+ """Binary operation: left op right"""
145
+ left: Expression = None
146
+ op: str = ""
147
+ right: Expression = None
148
+
149
+
150
+ @dataclass
151
+ class UnaryOp(Expression):
152
+ """Unary operation: op operand"""
153
+ op: str = ""
154
+ operand: Expression = None
155
+
156
+
157
+ @dataclass
158
+ class TernaryOp(Expression):
159
+ """Ternary operation: condition ? true_expr : false_expr"""
160
+ condition: Expression = None
161
+ true_expr: Expression = None
162
+ false_expr: Expression = None
163
+
164
+
165
+ @dataclass
166
+ class FunctionCall(Expression):
167
+ """Function call: func(args, kwargs)"""
168
+ func: Union[str, 'MemberAccess'] = "" # Can be "ta.sma" or MemberAccess node
169
+ args: List[Expression] = field(default_factory=list)
170
+ kwargs: dict = field(default_factory=dict)
171
+
172
+
173
+ @dataclass
174
+ class MethodCall(Expression):
175
+ """Method call: object.method(args)"""
176
+ object: Expression = None
177
+ method: str = ""
178
+ args: List[Expression] = field(default_factory=list)
179
+ kwargs: dict = field(default_factory=dict)
180
+
181
+
182
+ @dataclass
183
+ class MemberAccess(Expression):
184
+ """Member access: object.member"""
185
+ object: Union[str, Expression] = "" # Can be identifier string or expression
186
+ member: str = ""
187
+
188
+
189
+ @dataclass
190
+ class IndexAccess(Expression):
191
+ """Index access: object[index]"""
192
+ object: Expression = None
193
+ index: Expression = None
194
+
195
+
196
+ @dataclass
197
+ class ArrayLiteral(Expression):
198
+ """Array literal: [elem1, elem2, ...]"""
199
+ elements: List[Expression] = field(default_factory=list)
200
+
201
+
202
+ @dataclass
203
+ class TupleDestructure(Expression):
204
+ """Tuple destructuring: [a, b, c] = expr"""
205
+ names: List[str] = field(default_factory=list)
206
+ value: Expression = None
207
+
208
+
209
+ @dataclass
210
+ class Identifier(Expression):
211
+ """Identifier (variable name)"""
212
+ name: str = ""
213
+
214
+
215
+ @dataclass
216
+ class Literal(Expression):
217
+ """Literal value (int, float, string, bool, color)"""
218
+ value: Any = None
219
+ literal_type: str = "" # 'int', 'float', 'string', 'bool', 'color'
220
+ is_double_quoted: bool = False # Track if original source used double quotes
221
+
222
+
223
+ @dataclass
224
+ class NaLiteral(Expression):
225
+ """The 'na' literal value"""
226
+ pass
227
+
228
+
229
+ @dataclass
230
+ class IfExpression(Expression):
231
+ """If expression (returns a value): if cond\n val1\nelse\n val2"""
232
+ condition: Expression = None
233
+ true_expr: Union[Expression, List['Statement']] = None
234
+ false_expr: Optional[Union[Expression, List['Statement']]] = None
235
+
236
+
237
+ @dataclass
238
+ class SwitchExpression(Expression):
239
+ """Switch expression (returns a value)"""
240
+ expr: Optional[Expression] = None
241
+ cases: List[tuple[Expression, Union[Expression, List['Statement']]]] = field(default_factory=list)
242
+ default: Optional[Union[Expression, List['Statement']]] = None
243
+
244
+
245
+ # ============================================================================
246
+ # Statements
247
+ # ============================================================================
248
+
249
+ @dataclass
250
+ class Statement(ASTNode):
251
+ """Base class for all statements."""
252
+ pass
253
+
254
+
255
+ @dataclass
256
+ class Assignment(Statement):
257
+ """Assignment statement: target = value"""
258
+ target: Union[str, 'TupleDestructure'] = ""
259
+ value: Expression = None
260
+ type_hint: Optional[str] = None
261
+
262
+
263
+ @dataclass
264
+ class Reassignment(Statement):
265
+ """Reassignment statement: target := value"""
266
+ target: str = ""
267
+ value: Expression = None
268
+
269
+
270
+ @dataclass
271
+ class IfStatement(Statement):
272
+ """If statement: if cond\n body\nelse if ...\nelse ..."""
273
+ condition: Expression = None
274
+ body: List[Statement] = field(default_factory=list)
275
+ elseifs: List[tuple[Expression, List[Statement]]] = field(default_factory=list)
276
+ else_body: Optional[List[Statement]] = None
277
+
278
+
279
+ @dataclass
280
+ class ForLoop(Statement):
281
+ """For loop: for var = from to to_val [by step]"""
282
+ var: str = ""
283
+ from_val: Expression = None
284
+ to_val: Expression = None
285
+ step: Optional[Expression] = None
286
+ body: List[Statement] = field(default_factory=list)
287
+
288
+
289
+ @dataclass
290
+ class ForInLoop(Statement):
291
+ """For-in loop: for [vars] in iterable"""
292
+ vars: List[str] = field(default_factory=list)
293
+ iterable: Expression = None
294
+ body: List[Statement] = field(default_factory=list)
295
+
296
+
297
+ @dataclass
298
+ class WhileLoop(Statement):
299
+ """While loop: while condition\n body"""
300
+ condition: Expression = None
301
+ body: List[Statement] = field(default_factory=list)
302
+
303
+
304
+ @dataclass
305
+ class SwitchStatement(Statement):
306
+ """Switch statement (imperative form)"""
307
+ expr: Optional[Expression] = None
308
+ cases: List[tuple[Expression, List[Statement]]] = field(default_factory=list)
309
+ default: Optional[List[Statement]] = None
310
+
311
+
312
+ @dataclass
313
+ class BreakStatement(Statement):
314
+ """Break statement"""
315
+ pass
316
+
317
+
318
+ @dataclass
319
+ class ContinueStatement(Statement):
320
+ """Continue statement"""
321
+ pass
322
+
323
+
324
+ @dataclass
325
+ class ExpressionStatement(Statement):
326
+ """Expression as statement (e.g., function call on its own line)"""
327
+ expr: Expression = None
328
+
329
+
330
+ @dataclass
331
+ class ReturnStatement(Statement):
332
+ """Explicit return statement (for codegen)"""
333
+ expr: Optional[Expression] = None
334
+
335
+
336
+ @dataclass
337
+ class RawCode(Statement):
338
+ """Raw Python code to be emitted as-is by codegen."""
339
+ code: str = ""
340
+
341
+
342
+ # ============================================================================
343
+ # Type annotations
344
+ # ============================================================================
345
+
346
+ # Forward references for type checking
347
+ Statement = Union[
348
+ Assignment, Reassignment, IfStatement, ForLoop, ForInLoop,
349
+ WhileLoop, SwitchStatement, BreakStatement, ContinueStatement,
350
+ ExpressionStatement, ReturnStatement, VarDecl, VaripDecl,
351
+ FuncDecl, InputDecl
352
+ ]
pine2pyne/cli.py ADDED
@@ -0,0 +1,212 @@
1
+ """
2
+ Command-line interface for pine2pyne transpiler.
3
+ """
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import List, Optional
8
+ from . import transpile, __version__, _is_comment_only
9
+ from .errors import Pine2PyneError
10
+
11
+
12
+ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
13
+ """Parse command-line arguments."""
14
+ parser = argparse.ArgumentParser(
15
+ prog='pine2pyne',
16
+ description='Transpile Pine Script v6 to PyneCore Python',
17
+ formatter_class=argparse.RawDescriptionHelpFormatter,
18
+ epilog='''
19
+ Examples:
20
+ # Output to stdout
21
+ python -m pine2pyne strategy.pine
22
+
23
+ # Output to file
24
+ python -m pine2pyne strategy.pine -o strategy.py
25
+
26
+ # Validate syntax only
27
+ python -m pine2pyne strategy.pine --validate
28
+
29
+ # Batch convert multiple files
30
+ python -m pine2pyne strategy/*.pine -o output/
31
+ '''
32
+ )
33
+
34
+ parser.add_argument(
35
+ 'input',
36
+ nargs='+',
37
+ type=str,
38
+ help='Input Pine Script file(s) (.pine)'
39
+ )
40
+
41
+ parser.add_argument(
42
+ '-o', '--output',
43
+ type=str,
44
+ help='Output file or directory. If directory, outputs will have .py extension'
45
+ )
46
+
47
+ parser.add_argument(
48
+ '--validate',
49
+ action='store_true',
50
+ help='Validate syntax only (parse without generating code)'
51
+ )
52
+
53
+ parser.add_argument(
54
+ '-v', '--verbose',
55
+ action='store_true',
56
+ help='Enable verbose output'
57
+ )
58
+
59
+ parser.add_argument(
60
+ '--version',
61
+ action='version',
62
+ version=f'%(prog)s {__version__}'
63
+ )
64
+
65
+ return parser.parse_args(args)
66
+
67
+
68
+ def transpile_file(input_path: Path, output_path: Optional[Path], validate_only: bool, verbose: bool) -> bool:
69
+ """
70
+ Transpile a single Pine Script file.
71
+
72
+ Returns:
73
+ True if successful, False otherwise
74
+ """
75
+ try:
76
+ # Read input file
77
+ if verbose:
78
+ print(f'Reading {input_path}...')
79
+
80
+ source = input_path.read_text(encoding='utf-8')
81
+
82
+ if validate_only:
83
+ # Only parse to validate syntax
84
+ from .lexer import Lexer
85
+ from .parser import Parser
86
+
87
+ if verbose:
88
+ print('Tokenizing...')
89
+ lexer = Lexer(source)
90
+ tokens = lexer.tokenize()
91
+
92
+ if verbose:
93
+ print('Parsing...')
94
+ parser = Parser(tokens)
95
+ ast = parser.parse()
96
+
97
+ print(f'✓ {input_path} - Valid syntax')
98
+ return True
99
+
100
+ # Full transpilation
101
+ if verbose:
102
+ print('Transpiling...')
103
+
104
+ output_code = transpile(source)
105
+
106
+ # Empty output is only legitimate for a comment-only / empty source.
107
+ # If the source carries real Pine code but the transpiler produced
108
+ # nothing, an unsupported construct was silently dropped — fail loudly
109
+ # instead of reporting a bogus success (exit 0 with no file written).
110
+ if not output_code:
111
+ if _is_comment_only(source):
112
+ if verbose:
113
+ print(f'Skipping {input_path} (comment-only file)')
114
+ return True
115
+ print(f'✗ {input_path}: transpiler produced no output for non-comment '
116
+ f'source (unsupported construct silently dropped)', file=sys.stderr)
117
+ return False
118
+
119
+ # Write output
120
+ if output_path:
121
+ if verbose:
122
+ print(f'Writing to {output_path}...')
123
+ output_path.parent.mkdir(parents=True, exist_ok=True)
124
+ output_path.write_text(output_code, encoding='utf-8')
125
+ print(f'✓ {input_path} → {output_path}')
126
+ else:
127
+ # Output to stdout
128
+ print(output_code)
129
+
130
+ return True
131
+
132
+ except Pine2PyneError as e:
133
+ print(f'✗ {input_path}: {e}', file=sys.stderr)
134
+ return False
135
+ except FileNotFoundError:
136
+ print(f'✗ {input_path}: File not found', file=sys.stderr)
137
+ return False
138
+ except Exception as e:
139
+ print(f'✗ {input_path}: Unexpected error: {e}', file=sys.stderr)
140
+ if verbose:
141
+ import traceback
142
+ traceback.print_exc()
143
+ return False
144
+
145
+
146
+ def main(argv: Optional[List[str]] = None) -> int:
147
+ """Main entry point for CLI."""
148
+ args = parse_args(argv)
149
+
150
+ # Resolve input files
151
+ input_paths: List[Path] = []
152
+ for input_pattern in args.input:
153
+ path = Path(input_pattern)
154
+ if path.is_file():
155
+ input_paths.append(path)
156
+ elif '*' in input_pattern or '?' in input_pattern:
157
+ # Glob pattern
158
+ from glob import glob
159
+ for match in glob(input_pattern):
160
+ input_paths.append(Path(match))
161
+ else:
162
+ print(f'Error: {input_pattern} is not a file or valid pattern', file=sys.stderr)
163
+ return 1
164
+
165
+ if not input_paths:
166
+ print('Error: No input files found', file=sys.stderr)
167
+ return 1
168
+
169
+ # Determine output strategy
170
+ output_is_dir = False
171
+ output_base = None
172
+
173
+ if args.output:
174
+ output_path = Path(args.output)
175
+ if output_path.is_dir() or (not output_path.exists() and len(input_paths) > 1):
176
+ output_is_dir = True
177
+ output_base = output_path
178
+ elif len(input_paths) > 1:
179
+ print('Error: Multiple input files require output to be a directory', file=sys.stderr)
180
+ return 1
181
+ else:
182
+ output_base = output_path
183
+
184
+ # Process files
185
+ success_count = 0
186
+ fail_count = 0
187
+
188
+ for input_path in input_paths:
189
+ # Determine output path
190
+ if args.output is None:
191
+ output_path = None # stdout
192
+ elif output_is_dir:
193
+ output_name = input_path.stem + '.py'
194
+ output_path = output_base / output_name
195
+ else:
196
+ output_path = output_base
197
+
198
+ # Transpile
199
+ if transpile_file(input_path, output_path, args.validate, args.verbose):
200
+ success_count += 1
201
+ else:
202
+ fail_count += 1
203
+
204
+ # Print summary for multiple files
205
+ if len(input_paths) > 1:
206
+ print(f'\nSummary: {success_count} succeeded, {fail_count} failed')
207
+
208
+ return 0 if fail_count == 0 else 1
209
+
210
+
211
+ if __name__ == '__main__':
212
+ sys.exit(main())