Usage¶
Library¶
Simple parse, dump, unparse:
# SPDX-FileCopyrightText: 2025 Yunseong Hwang
# SPDX-License-Identifier: LGPL-3.0-or-later
from __future__ import annotations
from pynescript.ast import dump
from pynescript.ast import parse
from pynescript.ast import unparse
script_source = """
//@version=5
strategy("RSI Strategy", overlay=true)
length = input( 14 )
overSold = input( 30 )
overBought = input( 70 )
price = close
vrsi = ta.rsi(price, length)
co = ta.crossover(vrsi, overSold)
cu = ta.crossunder(vrsi, overBought)
if (not na(vrsi))
if (co)
strategy.entry("RsiLE", strategy.long, comment="RsiLE")
if (cu)
strategy.entry("RsiSE", strategy.short, comment="RsiSE")
//plot(strategy.equity, title="equity", color=color.red, linewidth=2, style=plot.style_areabr)
"""
tree = parse(script_source)
tree_dump = dump(tree, indent=2)
tree_unparsed = unparse(tree)
print("DUMP:")
print(tree_dump)
print()
print("UNPARSED:")
print(tree_unparsed)
print()
Traversing parsed AST nodes:
Note
Note that this script does not produce a working Python program; it only generates structurally similar code using Python syntax.
# SPDX-FileCopyrightText: 2025 Yunseong Hwang
# SPDX-License-Identifier: LGPL-3.0-or-later
from __future__ import annotations
import ast as pyast
import contextlib
import typing
from collections import ChainMap
import click
import pynescript.ast as pyneast
from pynescript.ast import NodeVisitor
class PinescriptToPynecoreScriptTransformer(NodeVisitor):
def __init__(self):
self._inputs = []
self._variables = ChainMap()
def enter_block(self):
self._variables = self._variables.new_child()
def exit_block(self):
self._variables = self._variables.parents
@contextlib.contextmanager
def local_block(self):
self.enter_block()
try:
yield
finally:
self.exit_block()
def visit_Script(self, node: pyneast.Script) -> pyast.Module:
body = []
for stmt in node.body:
result = self.visit(stmt)
if result is not None:
if isinstance(result, list):
body.extend(result)
else:
body.append(result)
decl_stmt = body[0]
if not isinstance(decl_stmt, pyast.Expr):
msg = "no declaration statement, first statement is not an expression"
raise ValueError(msg)
if not isinstance(decl_stmt.value, pyast.Call):
msg = "no declaration statement, first statement is not a function call"
raise ValueError(msg)
if not isinstance(decl_stmt.value.func, pyast.Name):
msg = "no declaration statement, function is not simple name"
raise ValueError(msg)
if decl_stmt.value.func.id not in {"indicator", "strategy", "library"}:
msg = f"no declaration statement, function name is not in [indicator, strategy, library] but {decl_stmt.value.func.id}"
raise ValueError(msg)
return pyast.Module(
body=body,
type_ignores=[],
)
def visit_Expression(self, node: pyneast.Expression) -> pyast.Expression:
body = self.visit(node.body)
return pyast.Expression(
body=body,
)
def visit_FunctionDef(self, node: pyneast.FunctionDef) -> pyast.FunctionDef:
with self.local_block():
name = node.name
args_and_defaults = [self.visit(arg) for arg in node.args]
args_and_defaults = typing.cast(list[tuple[pyast.arg, pyast.expr | None]], args_and_defaults)
args = [arg for arg, default in args_and_defaults]
defaults = [default for arg, default in args_and_defaults if default is not None]
args = pyast.arguments(
posonlyargs=[],
args=args,
kwonlyargs=[],
kw_defaults=[],
defaults=defaults,
)
body = [self.visit(stmt) for stmt in node.body]
decorator_list = []
func_def = pyast.FunctionDef(
name=name,
args=args,
body=body,
decorator_list=decorator_list,
)
return func_def
def visit_TypeDef(self, node: pyneast.TypeDef):
msg = f"unsupported node {node}"
raise ValueError(msg)
def visit_Assign(self, node: pyneast.Assign) -> pyast.Assign | pyast.AnnAssign:
var_type = node.type
var_mode = node.mode
target = self.visit(node.target)
value = self.visit(node.value)
return pyast.Assign(
targets=[target],
value=value,
)
def visit_ReAssign(self, node: pyneast.ReAssign) -> pyast.Assign | pyast.AnnAssign:
target = self.visit(node.target)
value = self.visit(node.value)
return pyast.Assign(
targets=[target],
value=value,
)
def visit_AugAssign(self, node: pyneast.AugAssign) -> pyast.AugAssign:
target = self.visit(node.target)
op = self.visit(node.op)
value = self.visit(node.value)
return pyast.AugAssign(
target=target,
op=op,
value=value,
)
def visit_Import(self, node: pyneast.Import) -> pyast.Import:
name = f"lib.{node.namespace}.{node.name}.v{node.version}"
asname = node.alias
return pyast.Import(
names=[
pyast.alias(
name=name,
asname=asname,
)
]
)
def visit_Expr(self, node: pyneast.Expr) -> pyast.Expr:
value = self.visit(node.value)
if isinstance(value, pyast.stmt):
return value
return pyast.Expr(value=value)
def visit_Break(self, node: pyneast.Break) -> pyast.Break:
return pyast.Break()
def visit_Continue(self, node: pyneast.Continue) -> pyast.Continue:
return pyast.Continue()
def visit_BoolOp(self, node: pyneast.BoolOp) -> pyast.BoolOp:
op = self.visit(node.op)
values = [self.visit(value) for value in node.values]
return pyast.BoolOp(
op=op,
values=values,
)
def visit_BinOp(self, node: pyneast.BinOp) -> pyast.BinOp:
left = self.visit(node.left)
op = self.visit(node.op)
right = self.visit(node.right)
return pyast.BinOp(
left=left,
op=op,
right=right,
)
def visit_UnaryOp(self, node: pyneast.UnaryOp) -> pyast.UnaryOp:
op = self.visit(node.op)
operand = self.visit(node.operand)
return pyast.UnaryOp(
op=op,
operand=operand,
)
def visit_Conditional(self, node: pyneast.Conditional) -> pyast.IfExp:
test = self.visit(node.test)
body = self.visit(node.body)
orelse = self.visit(node.orelse)
return pyast.IfExp(
test=test,
body=body,
orelse=orelse,
)
def visit_Compare(self, node: pyneast.Compare) -> pyast.Compare:
left = self.visit(node.left)
ops = [self.visit(op) for op in node.ops]
comparators = [self.visit(comp) for comp in node.comparators]
return pyast.Compare(
left=left,
ops=ops,
comparators=comparators,
)
def visit_Call(self, node: pyneast.Call) -> pyast.Call:
func = self.visit(node.func)
args_and_keywords = [self.visit(arg) for arg in node.args]
args = [arg for arg in args_and_keywords if isinstance(arg, pyast.expr)]
keywords = [keyword for keyword in args_and_keywords if isinstance(keyword, pyast.keyword)]
call = pyast.Call(
func=func,
args=args,
keywords=keywords,
)
return call
def visit_Constant(self, node: pyneast.Constant) -> pyast.Constant:
value = node.value
kind = None
constant = pyast.Constant(
value=value,
kind=kind,
)
return constant
def visit_Attribute(self, node: pyneast.Attribute) -> pyast.Attribute | pyast.Name:
value = self.visit(node.value)
attr = node.attr
ctx = self.visit(node.ctx)
return pyast.Attribute(
value=value,
attr=attr,
ctx=ctx,
)
def visit_Subscript(self, node: pyneast.Subscript) -> pyast.Subscript:
if not node.slice:
value = pyast.Name(
id="array",
ctx=pyast.Load(),
)
slice = self.visit(node.value)
ctx = self.visit(node.ctx)
return pyast.Subscript(
value=value,
slice=slice,
ctx=ctx,
)
value = self.visit(node.value)
slice = self.visit(node.slice)
ctx = self.visit(node.ctx)
return pyast.Subscript(
value=value,
slice=slice,
ctx=ctx,
)
def visit_Name(self, node: pyneast.Name) -> pyast.Name:
id = node.id
ctx = self.visit(node.ctx)
return pyast.Name(
id=id,
ctx=ctx,
)
def visit_Tuple(self, node: pyneast.Tuple) -> pyast.Tuple:
elts = [self.visit(elt) for elt in node.elts]
ctx = self.visit(node.ctx)
return pyast.Tuple(
elts=elts,
ctx=ctx,
)
def visit_ForTo(self, node: pyneast.ForTo) -> pyast.For:
with self.local_block():
target = self.visit(node.target)
body = [self.visit(stmt) for stmt in node.body]
start = self.visit(node.start)
end = self.visit(node.end)
step = self.visit(node.step) if node.step else None
args = [start, end]
if step:
args.append(step)
iter = pyast.Call(
func=pyast.Name(id="range", ctx=pyast.Load()),
args=args,
keywords=[],
)
orelse = []
return pyast.For(
target=target,
iter=iter,
body=body,
orelse=orelse,
)
def visit_ForIn(self, node: pyneast.ForIn) -> pyast.For:
with self.local_block():
target = self.visit(node.target)
body = [self.visit(stmt) for stmt in node.body]
iter = self.visit(node.iter)
orelse = []
return pyast.For(
target=target,
iter=iter,
body=body,
orelse=orelse,
)
def visit_While(self, node: pyneast.While) -> pyast.While:
with self.local_block():
test = self.visit(node.test)
body = [self.visit(stmt) for stmt in node.body]
orelse = []
return pyast.While(
test=test,
body=body,
orelse=orelse,
)
def visit_If(self, node: pyneast.If) -> pyast.If:
with self.local_block():
test = self.visit(node.test)
body = [self.visit(stmt) for stmt in node.body]
orelse = [self.visit(stmt) for stmt in node.orelse]
return pyast.If(
test=test,
body=body,
orelse=orelse,
)
def visit_Switch(self, node: pyneast.Switch) -> pyast.Match | pyast.If:
with self.local_block():
if node.subject:
subject = self.visit(node.subject)
cases = [self.visit(case) for case in node.cases]
return pyast.Match(
subject=subject,
cases=cases,
)
case = node.cases[-1]
case = typing.cast(pyneast.Case, case)
if case.pattern:
test = self.visit(case.pattern)
body = self.visit(case.body)
if_chain = pyast.If(
test=test,
body=body,
orelse=[],
)
else:
if_chain = self.visit(case.body)
for case in reversed(node.cases[:-1]):
case = typing.cast(pyneast.Case, case)
test = self.visit(case.pattern)
body = self.visit(case.body)
if_chain = pyast.If(
test=test,
body=body,
orelse=[if_chain],
)
if not isinstance(if_chain, pyast.If):
if_chain = pyast.If(
test=pyast.Constant(
value=True,
kind=None,
),
body=if_chain,
orelse=[],
)
return if_chain
def visit_Qualify(self, node: pyneast.Qualify) -> pyast.Subscript:
return pyast.Subscript(
value=self.visit(node.qualifier),
slice=self.visit(node.value),
ctx=pyast.Load(),
)
def visit_Specialize(self, node: pyneast.Specialize) -> pyast.expr:
value = self.visit(node.value)
slice = self.visit(node.args)
return pyast.Subscript(
value=value,
slice=slice,
ctx=pyast.Load(),
)
def visit_Var(self, node: pyneast.Var):
return pyast.Name("var", pyast.Load())
def visit_VarIp(self, node: pyneast.VarIp):
return pyast.Name("varip", pyast.Load())
def visit_Const(self, node: pyneast.Const):
return pyast.Name("const", pyast.Load())
def visit_Input(self, node: pyneast.Input):
return pyast.Name("input", pyast.Load())
def visit_Simple(self, node: pyneast.Simple):
return pyast.Name("simple", pyast.Load())
def visit_Series(self, node: pyneast.Series):
return pyast.Name("series", pyast.Load())
def visit_Load(self, node: pyneast.Load) -> pyast.Load:
return pyast.Load()
def visit_Store(self, node: pyneast.Store) -> pyast.Store:
return pyast.Store()
def visit_And(self, node: pyneast.And) -> pyast.And:
return pyast.And()
def visit_Or(self, node: pyneast.Or) -> pyast.Or:
return pyast.Or()
def visit_Add(self, node: pyneast.Add) -> pyast.Add:
return pyast.Add()
def visit_Sub(self, node: pyneast.Sub) -> pyast.Sub:
return pyast.Sub()
def visit_Mult(self, node: pyneast.Mult) -> pyast.Mult:
return pyast.Mult()
def visit_Div(self, node: pyneast.Div) -> pyast.Div:
return pyast.Div()
def visit_Mod(self, node: pyneast.Mod) -> pyast.Mod:
return pyast.Mod()
def visit_Not(self, node: pyneast.Not) -> pyast.Not:
return pyast.Not()
def visit_UAdd(self, node: pyneast.UAdd) -> pyast.UAdd:
return pyast.UAdd()
def visit_USub(self, node: pyneast.USub) -> pyast.USub:
return pyast.USub()
def visit_Eq(self, node: pyneast.Eq) -> pyast.Eq:
return pyast.Eq()
def visit_NotEq(self, node: pyneast.NotEq) -> pyast.NotEq:
return pyast.NotEq()
def visit_Lt(self, node: pyneast.Lt) -> pyast.Lt:
return pyast.Lt()
def visit_LtE(self, node: pyneast.LtE) -> pyast.LtE:
return pyast.LtE()
def visit_Gt(self, node: pyneast.Gt) -> pyast.Gt:
return pyast.Gt()
def visit_GtE(self, node: pyneast.GtE) -> pyast.GtE:
return pyast.GtE()
def visit_Param(self, node: pyneast.Param) -> tuple[pyast.arg, pyast.expr | None]:
arg = node.name
annotation = self.visit(node.type) if node.type else None
arg = pyast.arg(arg=arg, annotation=annotation)
default = self.visit(node.default) if node.default else None
return arg, default
def visit_Arg(self, node: pyneast.Arg) -> pyast.expr | pyast.keyword:
arg = node.name
value = self.visit(node.value)
return (
pyast.keyword(
arg=arg,
value=value,
)
if arg
else value
)
def visit_Case(self, node: pyneast.Case) -> pyast.match_case:
if not node.pattern:
pattern = pyast.MatchAs()
elif isinstance(node.pattern, pyneast.Constant):
value = node.pattern.value
if isinstance(value, bool):
pattern = pyast.MatchSingleton(value=value)
else:
value = pyast.Constant(value=value)
pattern = pyast.MatchValue(value=value)
else:
value = self.visit(node.pattern)
pattern = pyast.MatchValue(value=value)
body = [self.visit(stmt) for stmt in node.body]
return pyast.match_case(
pattern=pattern,
body=body,
)
def visit_Comment(self, node: pyneast.Comment):
msg = f"unsupported node {node}"
raise ValueError(msg)
def compile(content: str) -> str:
transformer = PinescriptToPynecoreScriptTransformer()
pynescript_ast = pyneast.parse(content)
pynecore_ast = transformer.visit_Script(pynescript_ast)
pynecore_ast = pyast.fix_missing_locations(pynecore_ast)
code = pyast.unparse(pynecore_ast)
return code
@click.command
@click.argument("filename", type=click.Path())
def main(filename):
with open(filename, encoding="utf-8") as f:
content = f.read()
click.echo(compile(content))
if __name__ == "__main__":
main()
Cli¶
pynescript¶
Usage
pynescript [OPTIONS] COMMAND [ARGS]...
Options
- --version¶
Show the version and exit.
download-builtin-scripts¶
Download builtin scripts.
Usage
pynescript download-builtin-scripts [OPTIONS]
Options
- --script-dir <script_dir>¶
Required Directory where scripts to be saved (like tests/data/builtin_scripts).
parse-and-dump¶
Parse pinescript file to AST tree.
Usage
pynescript parse-and-dump [OPTIONS] PATH
Options
- --encoding <encoding>¶
Text encoding of the file.
- --indent <indent>¶
Indentation with of an AST dump.
- --output-file <PATH>¶
Path to output dump file, defaults to standard output.
Arguments
- PATH¶
Required argument
parse-and-unparse¶
Parse pinescript file and unparse back to pinescript.
Usage
pynescript parse-and-unparse [OPTIONS] PATH
Options
- --encoding <encoding>¶
Text encoding of the file.
- --output-file <PATH>¶
Path to output dump file, defaults to standard output.
Arguments
- PATH¶
Required argument