ScriptRunner API

ScriptRunner is the core class for running PyneCore scripts from Python code. It processes OHLCV data bar-by-bar through a compiled Pine Script, yielding indicator values and trade results as they happen.

Quick Start

  from pathlib import Path
from pynecore.core.script_runner import ScriptRunner
from pynecore.core.syminfo import SymInfo
from pynecore.types.ohlcv import OHLCV

# Create data (see Data & SymInfo page for more options)
syminfo = SymInfo(
    prefix="BINANCE", ticker="BTCUSD", currency="USD", basecurrency="BTC",
    description="Bitcoin", period="60", type="crypto",
    mintick=0.01, pricescale=100, minmove=1, pointvalue=1.0,
    timezone="UTC", volumetype="base",
    opening_hours=[], session_starts=[], session_ends=[],
)

candles = [
    OHLCV(timestamp=1704067200, open=42000, high=42500, low=41800, close=42300, volume=1000),
    OHLCV(timestamp=1704070800, open=42300, high=42800, low=42100, close=42600, volume=1200),
    # ... more bars
]

# Run an indicator
runner = ScriptRunner(
    script_path=Path("my_indicator.py"),
    ohlcv_iter=candles,
    syminfo=syminfo,
)

for candle, plot_data in runner.run_iter():
    rsi = plot_data.get("RSI")
    print(f"Close={candle.close:.2f}  RSI={rsi}")
  

Constructor

  ScriptRunner(
    script_path: Path,
ohlcv_iter: Iterable[OHLCV],
syminfo: SymInfo,
*,
plot_path: Path | None = None,
strat_path: Path | None = None,
trade_path: Path | None = None,
update_syminfo_every_run: bool = False,
last_bar_index: int = 0,
inputs: dict[str, Any] | None = None,
security_data: dict[str, str | Path] | None = None,
)
  

Parameters

ParameterTypeDescription
script_pathPathPath to a compiled PyneCore script (.py with @pyne marker)
ohlcv_iterIterable[OHLCV]Any iterable of OHLCV objects — list, generator, reader, etc.
syminfoSymInfoSymbol information (from TOML or manually created)
plot_pathPath | NoneSave indicator plot data to CSV
strat_pathPath | NoneSave strategy statistics to CSV
trade_pathPath | NoneSave trade-by-trade data to CSV
update_syminfo_every_runboolRe-apply syminfo before each bar (for parallel runners)
last_bar_indexintOverride last bar index (for multi-script setups)
inputsdict | NoneOverride script input() defaults at runtime
security_datadict | NoneOHLCV paths for request.security() contexts (see below)

Overriding Inputs

The inputs parameter lets you change script parameters without editing the script file:

  runner = ScriptRunner(
    script_path=Path("sma_crossover.py"),
    ohlcv_iter=candles,
    syminfo=syminfo,
    inputs={"Length": 20, "Confirm bars": 3},  # override input() defaults
)
  

Keys must match the title parameter of input() calls in the script. If a key doesn’t match any input, it’s silently ignored.

Providing Security Data

If your script uses request.security() to fetch data from other symbols or timeframes, you must provide the OHLCV data files for each security context via the security_data parameter.

Keys can be in two formats:

  • "TIMEFRAME" — matches any security context with that timeframe (e.g., "1D", "1W")
  • "SYMBOL:TIMEFRAME" — matches a specific symbol and timeframe (e.g., "AAPL:1H")

Values are paths to .ohlcv data files (with corresponding .toml syminfo files in the same directory).

  # Script that uses request.security() for daily data
runner = ScriptRunner(
    script_path=Path("multi_tf_indicator.py"),
    ohlcv_iter=candles_5m,  # chart data: 5-minute bars
    syminfo=syminfo,
    security_data={
        "1D": "data/EURUSD_1D",  # daily bars for same symbol
    },
)

# Script that fetches data from multiple symbols
runner = ScriptRunner(
    script_path=Path("advance_decline.py"),
    ohlcv_iter=candles,
    syminfo=syminfo,
    security_data={
        "USI:ADVN.NY": "data/USI_ADVN_NY",  # advancing issues
        "USI:DECL.NY": "data/USI_DECL_NY",  # declining issues
    },
)
  

Each OHLCV path should point to a directory base name (without extension). The system expects both <path>.ohlcv (binary data) and <path>.toml (symbol info) to exist.

Note: Security contexts spawn separate OS processes. Each process re-imports the script, loads its own OHLCV data, and builds Series history from bar 0. For technical details, see request.security() Internals.

run_iter() — Processing Bars

The primary method. Returns an iterator that yields results for each bar processed.

Indicators

Indicators yield a 2-tuple: (candle, plot_data).

  for candle, plot_data in runner.run_iter():
    # candle: the OHLCV object for this bar
    # plot_data: dict of values from plot() calls in the script

    rsi = plot_data.get("RSI")  # float, or None during warmup
    basis = plot_data.get("Basis")  # keys match plot() title parameter
  

Strategies

Strategies yield a 3-tuple: (candle, plot_data, new_trades).

  for candle, plot_data, new_trades in runner.run_iter():
    # candle: the OHLCV object for this bar
    # plot_data: dict of plotted values
    # new_trades: list of trades that CLOSED on this bar

    for trade in new_trades:
        direction = "LONG" if trade.size > 0 else "SHORT"
        print(f"{direction}  P&L={trade.profit:+.2f}")
  

Note: new_trades contains only trades that closed on the current bar, not open positions. Each trade appears exactly once — on the bar where it exits.

NA Values During Warmup

During the warmup period (first N bars where the indicator doesn’t have enough data), plot values are NA objects — PyneCore’s equivalent of Pine Script’s na.

NA works transparently — no special handling needed:

  • Comparisons return False: NA < 30, NA > 70, NA == x → all False
  • Arithmetic propagates: NA + 1NA, NA * 2.0NA
  • Format strings work: f"{na_value:.2f}""NaN"
  for candle, plot_data in runner.run_iter():
    rsi = plot_data.get("RSI")

    if rsi > 70:  # False when rsi is NA — no crash, no special check needed
        print(f"Overbought: RSI={rsi:.2f}")

    # NA values print as "NaN" in f-strings
    print(f"RSI={rsi:.2f}")  # "RSI=NaN" during warmup, "RSI=65.32" after
  

Trade Object

Trades returned by strategies have the following fields:

FieldTypeDescription
sizefloatQuantity (positive=long, negative=short)
entry_idstrID from strategy.entry() call
entry_bar_indexintBar index where entry filled
entry_timeintEntry timestamp (milliseconds)
entry_pricefloatFill price for entry
entry_commentstrComment from strategy.entry()
exit_idstrID from exit call
exit_bar_indexintBar index where exit filled
exit_timeintExit timestamp (milliseconds)
exit_pricefloatFill price for exit
profitfloatAbsolute P&L in account currency
profit_percentfloatP&L as percentage
cum_profitfloatCumulative P&L up to this trade
cum_profit_percentfloatCumulative P&L %
max_runupfloatMax unrealized profit during trade
max_runup_percentfloatMax runup %
max_drawdownfloatMax unrealized loss during trade
max_drawdown_percentfloatMax drawdown %
commissionfloatFees paid

Saving Output to CSV

You can write results to CSV files (same format as the CLI pyne run command):

  runner = ScriptRunner(
    script_path=Path("my_strategy.py"),
    ohlcv_iter=candles,
    syminfo=syminfo,
    plot_path=Path("output/plot.csv"),  # indicator values per bar
    strat_path=Path("output/stats.csv"),  # strategy statistics summary
    trade_path=Path("output/trades.csv"),  # trade-by-trade details
)

# Must exhaust the iterator for files to be written
for candle, plot_data, new_trades in runner.run_iter():
    pass  # files are written as bars are processed
  

Complete Example: Strategy with Trade Analysis

  from pathlib import Path
from pynecore.core.script_runner import ScriptRunner
from pynecore.core.data_converter import DataConverter
from pynecore.core.ohlcv_file import OHLCVReader
from pynecore.core.syminfo import SymInfo

# Convert CSV data to OHLCV format
csv_path = Path("data/EURUSD_1h.csv")
DataConverter().convert_to_ohlcv(csv_path)

# Load converted data
ohlcv_path = csv_path.with_suffix(".ohlcv")
toml_path = csv_path.with_suffix(".toml")
syminfo = SymInfo.load_toml(toml_path)

with OHLCVReader(ohlcv_path) as reader:
    runner = ScriptRunner(
        script_path=Path("sma_crossover.py"),
        ohlcv_iter=reader.read_from(reader.start_timestamp, reader.end_timestamp),
        syminfo=syminfo,
        inputs={"Length": 20, "Confirm bars": 2},
    )

    all_trades = []
    for candle, plot_data, new_trades in runner.run_iter():
        all_trades.extend(new_trades)

# Analyze results
if all_trades:
    wins = [t for t in all_trades if t.profit > 0]
    total_pnl = sum(t.profit for t in all_trades)
    print(f"Trades: {len(all_trades)}  Win rate: {len(wins) / len(all_trades) * 100:.1f}%  P&L: {total_pnl:+.2f}")